├── Block ├── Adminhtml │ └── Widget │ │ └── Ui │ │ └── Components.php └── Widget │ ├── AbstractWidget.php │ └── ExampleOfUIComponents.php ├── Component ├── EntitiesSelector.php └── Widget │ └── Form.php ├── DataProvider └── Widget │ ├── DataProvider.php │ └── DataProviderInterface.php ├── LICENSE ├── Layout └── Generic.php ├── Model ├── Base64.php ├── DecodeComponentValue.php ├── JsConfiguration │ ├── ModifierComposite.php │ ├── ModifierInterface.php │ └── Modifiers │ │ ├── AbstractModifier.php │ │ ├── ListingComponentModifier.php │ │ └── MultiselectComponentModifier.php ├── UiComponentGenerator.php └── Widget │ ├── AdditionalPreparer │ ├── PreparerInterface.php │ └── WysiwygPreparer.php │ ├── Metadata.php │ ├── MetadataInterface.php │ ├── PrepareUiComponent.php │ └── UiComponentFactory.php ├── Plugin └── Magento │ ├── Framework │ └── View │ │ └── Layout │ │ └── Generic │ │ └── ModifyJsConfiguration.php │ └── Widget │ └── Block │ └── Adminhtml │ └── Widget │ └── Options │ └── DecodeComponentValues.php ├── README.md ├── ViewModel └── PageBuilder.php ├── composer.json ├── docs ├── ui-components │ └── entities-selector │ │ ├── README.md │ │ ├── demo.gif │ │ ├── demo_screen_1.png │ │ ├── demo_screen_2.png │ │ └── demo_screen_3.png └── widgets │ └── using-ui-components │ ├── README.md │ ├── demo.gif │ └── result.png ├── etc ├── di.xml ├── module.xml └── widget.xml ├── registration.php └── view └── adminhtml ├── layout ├── adminhtml_widget_instance_edit.xml ├── adminhtml_widget_loadoptions.xml └── default.xml ├── requirejs-config.js ├── templates └── widget │ ├── form │ └── container.phtml │ └── initialize-ui-components.phtml ├── ui_component └── widget_example_form.xml └── web ├── js ├── model │ ├── messageList.js │ └── messages.js ├── utils │ └── base64.js ├── view │ ├── components │ │ ├── entities-selector.js │ │ └── entities-selector │ │ │ └── insert-listing │ │ │ └── renderer.js │ ├── grid │ │ └── columns │ │ │ └── multiselect-with-limit.js │ └── messages.js ├── widget │ ├── events.js │ ├── form │ │ └── element │ │ │ ├── page-builder │ │ │ └── wysiwyg.js │ │ │ ├── sync-field.js │ │ │ └── wysiwyg.js │ └── initialization │ │ ├── main.js │ │ └── scripts.js └── wysiwyg │ └── widget.js └── template ├── entities-selector └── complex.html ├── messages.html └── widget └── form └── element └── hidden.html /Block/Adminhtml/Widget/Ui/Components.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 89 | $this->contextFactory = $contextFactory; 90 | $this->uiComponentFactory = $uiComponentFactory; 91 | $this->structure = $structure; 92 | $this->structureGenerator = $structureGenerator; 93 | $this->metadataFactory = $metadataFactory; 94 | $this->prepareUiComponent = $prepareUiComponent; 95 | 96 | if (empty($this->getNamespace())) { 97 | throw new LocalizedException(__('`namespace` is required.')); 98 | } 99 | } 100 | 101 | /** 102 | * Prepare element HTML 103 | * 104 | * @param AbstractElement $element 105 | * @return AbstractElement 106 | * @throws LocalizedException 107 | */ 108 | public function prepareElementHtml(AbstractElement $element): AbstractElement 109 | { 110 | $component = $this->uiComponentFactory->create( 111 | $this->getNamespace(), 112 | null, 113 | [ 114 | 'context' => $this->getContext(), 115 | 'metadata' => $this->generateMetadata($element), 116 | 'structure' => $this->structure 117 | ] 118 | ); 119 | $this->prepareUiComponent->execute($component); 120 | $configuration = $this->structureGenerator->generate($component); 121 | 122 | $element->setData('after_element_html', $this->render($configuration)); 123 | $element->setData('value', ''); 124 | $element->setType('hidden'); 125 | 126 | return $element; 127 | } 128 | 129 | /** 130 | * Generate metadata 131 | * 132 | * @param AbstractElement $element 133 | * @return MetadataInterface 134 | */ 135 | protected function generateMetadata(AbstractElement $element): MetadataInterface 136 | { 137 | $value = $element->getValue(); 138 | if (!is_array($value)) { 139 | $value = []; 140 | } 141 | 142 | /** @var MetadataInterface $metadata */ 143 | $metadata = $this->metadataFactory->create(); 144 | $metadata->setFormData($value); 145 | $metadata->setSyncFieldName($element->getName()); 146 | 147 | return $metadata; 148 | } 149 | 150 | /** 151 | * Get context 152 | * 153 | * @return ContextInterface 154 | * @throws LocalizedException 155 | */ 156 | protected function getContext(): ContextInterface 157 | { 158 | if (!$this->context) { 159 | $this->context = $this->contextFactory->create([ 160 | 'namespace' => $this->getNamespace(), 161 | 'pageLayout' => $this->getLayout() 162 | ]); 163 | } 164 | 165 | return $this->context; 166 | } 167 | 168 | /** 169 | * Render ui components 170 | * 171 | * @param array $configuration 172 | * @return string 173 | */ 174 | public function render(array $configuration): string 175 | { 176 | $wrappedContent = $this->wrapContent( 177 | $this->serializer->serialize($configuration) 178 | ); 179 | $scope = $this->getScope(); 180 | 181 | return << 186 | 187 | 188 | $wrappedContent 189 | EOT; 190 | } 191 | 192 | /** 193 | * Get scope 194 | * 195 | * @return string 196 | */ 197 | public function getScope(): string 198 | { 199 | return $this->getNamespace() . '.' . $this->getNamespace(); 200 | } 201 | 202 | /** 203 | * Wrap content 204 | * 205 | * @param string $content 206 | * @return string 207 | */ 208 | protected function wrapContent(string $content): string 209 | { 210 | return '' 211 | . '{"*": {"Magento_Ui/js/core/app": ' . $content . '}}' 212 | . ''; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Block/Widget/AbstractWidget.php: -------------------------------------------------------------------------------- 1 | decodeComponentValue = $decodeComponentValue; 30 | } 31 | 32 | /** 33 | * Get data 34 | * 35 | * @param string $key 36 | * @param string|int $index 37 | * @return array|mixed|string|null 38 | */ 39 | public function getData($key = '', $index = null) 40 | { 41 | $result = parent::getData($key, $index); 42 | 43 | if (!$result || !is_string($result)) { 44 | return $result; 45 | } 46 | 47 | return $this->decodeComponentValue->execute($result); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Block/Widget/ExampleOfUIComponents.php: -------------------------------------------------------------------------------- 1 | getData('component_data'); 18 | // phpcs:ignore Magento2.Functions.DiscouragedFunction 19 | print_r($data); 20 | 21 | return ''; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Component/EntitiesSelector.php: -------------------------------------------------------------------------------- 1 | . 26 | */ 27 | declare(strict_types=1); 28 | 29 | namespace Grasch\AdminUi\Component; 30 | 31 | use Grasch\AdminUi\Model\UiComponentGenerator; 32 | use Magento\Framework\App\ObjectManager; 33 | use Magento\Framework\Exception\LocalizedException; 34 | use Magento\Framework\Stdlib\ArrayManager; 35 | use Magento\Framework\UrlInterface; 36 | use Magento\Framework\View\Element\UiComponent\ContextInterface; 37 | use Magento\Ui\Component\AbstractComponent; 38 | use Magento\Ui\Component\DynamicRows; 39 | use Magento\Ui\Component\Form\Element\DataType\Number; 40 | use Magento\Ui\Component\Form\Element\DataType\Text; 41 | use Magento\Ui\Component\Form\Element\Input; 42 | use Magento\Ui\Component\Form\Field; 43 | use Magento\Ui\Component\Modal; 44 | 45 | class EntitiesSelector extends AbstractComponent 46 | { 47 | public const NAME = 'entitiesSelector'; 48 | 49 | protected const COLUMN_FIELD_ORIGINAL_NAME = 'original_name'; 50 | protected const COLUMN_FIELD_TYPE = 'type'; 51 | protected const COLUMN_FIELD_LABEL = 'label'; 52 | protected const COLUMN_FIELD_SORT_ORDER = 'sortOrder'; 53 | protected const COLUMN_FIELD_FIT = 'fit'; 54 | 55 | /** 56 | * @var UrlInterface 57 | */ 58 | protected UrlInterface $urlBuilder; 59 | 60 | /** 61 | * @var UiComponentGenerator 62 | */ 63 | protected UiComponentGenerator $uiComponentGenerator; 64 | 65 | /** 66 | * @var array|string[] 67 | */ 68 | protected array $_requiredColumnFields = [ 69 | self::COLUMN_FIELD_ORIGINAL_NAME, 70 | self::COLUMN_FIELD_TYPE, 71 | self::COLUMN_FIELD_LABEL, 72 | self::COLUMN_FIELD_SORT_ORDER, 73 | self::COLUMN_FIELD_FIT 74 | ]; 75 | 76 | /** 77 | * @var array 78 | */ 79 | protected array $_map = []; 80 | 81 | /** 82 | * @var array 83 | */ 84 | protected array $_columnsConfig = []; 85 | 86 | /** 87 | * @var ArrayManager|mixed 88 | */ 89 | private ArrayManager $arrayManager; 90 | 91 | /** 92 | * @param ContextInterface $context 93 | * @param UrlInterface $urlBuilder 94 | * @param UiComponentGenerator $uiComponentGenerator 95 | * @param array $components 96 | * @param array $data 97 | * @param array $requiredColumnFields 98 | * @throws LocalizedException 99 | */ 100 | public function __construct( 101 | ContextInterface $context, 102 | UrlInterface $urlBuilder, 103 | UiComponentGenerator $uiComponentGenerator, 104 | ArrayManager $arrayManager = null, 105 | array $components = [], 106 | array $data = [], 107 | array $requiredColumnFields = [] 108 | ) { 109 | parent::__construct( 110 | $context, 111 | $components, 112 | $data 113 | ); 114 | 115 | $this->urlBuilder = $urlBuilder; 116 | $this->uiComponentGenerator = $uiComponentGenerator; 117 | $this->arrayManager = $arrayManager ?? ObjectManager::getInstance()->create(ArrayManager::class); 118 | 119 | $this->_requiredColumnFields = array_merge( 120 | $this->_requiredColumnFields, 121 | $requiredColumnFields 122 | ); 123 | 124 | $this->_initColumnsConfig(); 125 | } 126 | 127 | /** 128 | * Init columns config 129 | * 130 | * @throws LocalizedException 131 | */ 132 | protected function _initColumnsConfig() 133 | { 134 | $columns = $this->getRequiredConfigValueByPath('grid/columns'); 135 | if (empty($columns)) { 136 | throw new LocalizedException(__( 137 | '"config/grid/columns" can not be empty for the "%2" field.', 138 | $this->getName() 139 | )); 140 | } 141 | 142 | foreach ($columns as $columnName => $columnConfig) { 143 | foreach ($this->_requiredColumnFields as $requiredField) { 144 | $this->getRequiredConfigValueByPath( 145 | 'grid/columns/' . $columnName . '/' . $requiredField 146 | ); 147 | } 148 | 149 | $this->_map[$columnName] = $columnConfig[self::COLUMN_FIELD_ORIGINAL_NAME]; 150 | $this->_columnsConfig[$columnName] = $columnConfig; 151 | } 152 | } 153 | 154 | /** 155 | * @inheritDoc 156 | */ 157 | public function getComponentName(): string 158 | { 159 | return static::NAME; 160 | } 161 | 162 | /** 163 | * @inheritDoc 164 | */ 165 | public function prepare() 166 | { 167 | $this->uiComponentGenerator->generateChildComponentsFromArray( 168 | $this, 169 | $this->getChildComponentsAsArray() 170 | ); 171 | 172 | parent::prepare(); 173 | } 174 | 175 | /** 176 | * @return array 177 | * @throws LocalizedException 178 | */ 179 | public function getChildComponentsAsArrayForMeta(): array 180 | { 181 | $data = $this->getChildComponentsAsArray(); 182 | 183 | $paths = $this->arrayManager->findPaths('data', $data); 184 | foreach ($paths as $path) { 185 | $value = ['data' => $this->arrayManager->get($path, $data)]; 186 | $newPath = str_replace('/data', '/arguments', $path); 187 | $data = $this->arrayManager->remove($path, $data); 188 | $data = $this->arrayManager->set($newPath, $data, $value); 189 | } 190 | 191 | return $data; 192 | } 193 | 194 | /** 195 | * Get child components as array 196 | * 197 | * @return array 198 | * @throws LocalizedException 199 | */ 200 | public function getChildComponentsAsArray(): array 201 | { 202 | return [ 203 | 'modal' => $this->getGenericModal(), 204 | 'button_set' => $this->getButtonSet(), 205 | $this->getName() => $this->getGrid(), 206 | 'listing-renderer' => $this->getListingRenderer() 207 | ]; 208 | } 209 | 210 | /** 211 | * Get grid component 212 | * 213 | * @return array 214 | * @throws LocalizedException 215 | */ 216 | public function getGrid(): array 217 | { 218 | $dndEnabled = 219 | $this->getConfigByPath('grid/dndEnabled') ?? true; 220 | 221 | return [ 222 | 'data' => [ 223 | 'componentClassName' => DynamicRows::class, 224 | 'config' =>[ 225 | 'additionalClasses' => 'admin__field-wide', 226 | 'componentType' => DynamicRows::NAME, 227 | 'label' => null, 228 | 'columnsHeader' => false, 229 | 'columnsHeaderAfterRender' => true, 230 | 'renderDefaultRecord' => false, 231 | 'template' => 'ui/dynamic-rows/templates/grid', 232 | 'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid', 233 | 'addButton' => false, 234 | 'recordTemplate' => 'record', 235 | 'dataScope' => '', 236 | 'deleteButtonLabel' => __('Remove'), 237 | 'dataProvider' => $this->generateUniqNamespace(), 238 | 'map' => $this->_map, 239 | 'dndConfig' => [ 240 | 'enabled' => $dndEnabled 241 | ], 242 | 'links' => [ 243 | 'insertData' => 244 | '${ $.provider }:${ $.dataProvider }', 245 | '__disableTmpl' => ['insertData' => false] 246 | ], 247 | 'sortOrder' => 2, 248 | ] 249 | ], 250 | 'context' => $this->context, 251 | 'children' => [ 252 | 'record' => [ 253 | 'data' => [ 254 | 'config' => [ 255 | 'componentType' => 'container', 256 | 'isTemplate' => true, 257 | 'is_collection' => true, 258 | 'component' => 'Magento_Ui/js/dynamic-rows/record', 259 | 'dataScope' => '' 260 | ] 261 | ], 262 | 'context' => $this->context, 263 | 'children' => $this->fillMeta() 264 | ] 265 | ] 266 | ]; 267 | } 268 | 269 | /** 270 | * Fill meta 271 | * 272 | * @return array[] 273 | */ 274 | public function fillMeta(): array 275 | { 276 | $columns = []; 277 | 278 | foreach ($this->_columnsConfig as $columnName => $columnConfig) { 279 | $method = 'get' . ucfirst($columnConfig[self::COLUMN_FIELD_TYPE]) . 'Column'; 280 | $columns[] = $this->{$method}($columnName, $columnConfig); 281 | } 282 | 283 | $columns[] = $this->getActionDeleteColumn(); 284 | $columns[] = $this->getPositionColumn(); 285 | 286 | return array_merge(...$columns); 287 | } 288 | 289 | /** 290 | * Get text column component 291 | * 292 | * @param string $columnName 293 | * @param array $columnConfig 294 | * @return array 295 | */ 296 | public function getTextColumn( 297 | string $columnName, 298 | array $columnConfig 299 | ): array { 300 | return [ 301 | $columnName => [ 302 | 'data' => [ 303 | 'config' => [ 304 | 'componentType' => Field::NAME, 305 | 'formElement' => Input::NAME, 306 | 'elementTmpl' => 'ui/dynamic-rows/cells/text', 307 | 'component' => 'Magento_Ui/js/form/element/text', 308 | 'template' => 'ui/form/field', 309 | 'dataType' => Text::NAME, 310 | 'dataScope' => $columnName, 311 | 'fit' => $columnConfig[self::COLUMN_FIELD_FIT], 312 | 'label' => $columnConfig[self::COLUMN_FIELD_LABEL], 313 | 'sortOrder' => $columnConfig[self::COLUMN_FIELD_SORT_ORDER], 314 | ] 315 | ], 316 | 'context' => $this->context 317 | ] 318 | ]; 319 | } 320 | 321 | /** 322 | * Get thumbnail column component 323 | * 324 | * @param string $columnName 325 | * @param array $columnConfig 326 | * @return array 327 | */ 328 | public function getThumbnailColumn( 329 | string $columnName, 330 | array $columnConfig 331 | ): array { 332 | return [ 333 | $columnName => [ 334 | 'data' => [ 335 | 'config' => [ 336 | 'componentType' => Field::NAME, 337 | 'formElement' => Input::NAME, 338 | 'elementTmpl' => 'ui/dynamic-rows/cells/thumbnail', 339 | 'component' => 'Magento_Ui/js/form/element/abstract', 340 | 'template' => 'ui/form/field', 341 | 'dataType' => Text::NAME, 342 | 'dataScope' => $columnName, 343 | 'fit' => $columnConfig[self::COLUMN_FIELD_FIT], 344 | 'label' => $columnConfig[self::COLUMN_FIELD_LABEL], 345 | 'sortOrder' => $columnConfig[self::COLUMN_FIELD_SORT_ORDER], 346 | ] 347 | ], 348 | 'context' => $this->context 349 | ] 350 | ]; 351 | } 352 | 353 | /** 354 | * Get action delete column component 355 | * 356 | * @return array 357 | */ 358 | public function getActionDeleteColumn(): array 359 | { 360 | return [ 361 | 'actionDelete' => [ 362 | 'data' => [ 363 | 'config' => [ 364 | 'template' => 'ui/dynamic-rows/cells/action-delete', 365 | 'elementTmpl' => 'ui/form/element/input', 366 | 'component' => 'Magento_Ui/js/dynamic-rows/action-delete', 367 | 'additionalClasses' => 'data-grid-actions-cell', 368 | 'componentType' => 'actionDelete', 369 | 'dataType' => Text::NAME, 370 | 'label' => __('Actions'), 371 | 'sortOrder' => 70, 372 | 'fit' => true, 373 | ] 374 | ], 375 | 'context' => $this->context 376 | ] 377 | ]; 378 | } 379 | 380 | /** 381 | * Get position column component 382 | * 383 | * @return array 384 | */ 385 | public function getPositionColumn(): array 386 | { 387 | return [ 388 | 'position' => [ 389 | 'data' => [ 390 | 'config' => [ 391 | 'component' => 'Magento_Catalog/js/form/element/input', 392 | 'template' => 'ui/form/field', 393 | 'elementTmpl' => 'ui/form/element/input', 394 | 'dataType' => Number::NAME, 395 | 'formElement' => Input::NAME, 396 | 'componentType' => Field::NAME, 397 | 'dataScope' => 'position', 398 | 'visible' => false, 399 | ] 400 | ], 401 | 'context' => $this->context 402 | ] 403 | ]; 404 | } 405 | 406 | /** 407 | * Get button set component 408 | * 409 | * @return array 410 | * @throws LocalizedException 411 | */ 412 | public function getButtonSet(): array 413 | { 414 | $content = 415 | $this->getConfigByPath('button_set/main_title') ?? __('Select Items'); 416 | $buttonTitle = 417 | $this->getConfigByPath('button_set/button_add/title') ?? __('Add Items'); 418 | $additionalButtonAddActions = 419 | $this->getConfigByPath('button_set/button_add/additionalActions') ?? []; 420 | 421 | return [ 422 | 'data' => [ 423 | 'config' => [ 424 | 'formElement' => 'container', 425 | 'componentType' => 'container', 426 | 'label' => $this->getRequiredConfigValueByPath('label'), 427 | 'content' => $content, 428 | 'template' => 'Grasch_AdminUi/entities-selector/complex', 429 | 'component' => 'uiComponent' 430 | ] 431 | ], 432 | 'context' => $this->context, 433 | 'children' => [ 434 | 'button_add' => [ 435 | 'data' => [ 436 | 'config' => [ 437 | 'formElement' => 'container', 438 | 'componentType' => 'container', 439 | 'component' => 'Magento_Ui/js/form/components/button', 440 | 'title' => $buttonTitle, 441 | 'provider' => null, 442 | 'actions' => array_merge( 443 | [ 444 | [ 445 | 'targetName' => '${ $.parentName.replace(".button_set", "") }.modal', 446 | '__disableTmpl' => ['targetName' => false], 447 | 'actionName' => 'toggleModal', 448 | ], 449 | [ 450 | 'targetName' => 451 | '${ $.parentName.replace(".button_set", "") }.listing-renderer', 452 | '__disableTmpl' => ['targetName' => false], 453 | 'actionName' => 'render', 454 | ], 455 | ], 456 | $additionalButtonAddActions 457 | ) 458 | ] 459 | ], 460 | 'context' => $this->context 461 | ] 462 | ] 463 | ]; 464 | } 465 | 466 | /** 467 | * Get selections provider 468 | * 469 | * @return string 470 | * @throws LocalizedException 471 | */ 472 | protected function getSelectionsProvider(): string 473 | { 474 | $namespace = $this->getRequiredConfigValueByPath('namespace'); 475 | 476 | return sprintf( 477 | '%s.%s.%s.%s', 478 | $namespace, 479 | $namespace, 480 | $this->getRequiredConfigValueByPath('columnsName'), 481 | $this->getRequiredConfigValueByPath('selectionsColumnName') 482 | ); 483 | } 484 | 485 | /** 486 | * Get columns provider 487 | * 488 | * @return string 489 | * @throws LocalizedException 490 | */ 491 | protected function getColumnsProvider(): string 492 | { 493 | $namespace = $this->getRequiredConfigValueByPath('namespace'); 494 | 495 | return sprintf( 496 | '%s.%s.%s', 497 | $namespace, 498 | $namespace, 499 | $this->getRequiredConfigValueByPath('columnsName') 500 | ); 501 | } 502 | 503 | /** 504 | * Get external provider 505 | * 506 | * @return string 507 | * @throws LocalizedException 508 | */ 509 | protected function getExternalProvider(): string 510 | { 511 | $namespace = $this->getRequiredConfigValueByPath('namespace'); 512 | 513 | return sprintf('%s.%s_data_source', $namespace, $namespace); 514 | } 515 | 516 | /** 517 | * Get listing renderer component 518 | * 519 | * @return array 520 | * @throws LocalizedException 521 | */ 522 | public function getListingRenderer(): array 523 | { 524 | return [ 525 | 'data' => [ 526 | 'config' => [ 527 | 'componentType' => 'container', 528 | 'component' => 'Grasch_AdminUi/js/view/components/entities-selector/insert-listing/renderer', 529 | 'selectionsProvider' => $this->getSelectionsProvider(), 530 | 'columnsProvider' => $this->getColumnsProvider(), 531 | 'insertListing' => 'uniq_ns = ' . $this->generateUniqNamespace(), 532 | 'grid' => '${ $.parentName }.' . $this->getName(), 533 | '__disableTmpl' => ['grid' => false], 534 | ] 535 | ], 536 | 'context' => $this->context 537 | ]; 538 | } 539 | 540 | /** 541 | * Get generic modal component 542 | * 543 | * @return array 544 | * @throws LocalizedException 545 | */ 546 | public function getGenericModal(): array 547 | { 548 | $namespace = $this->getRequiredConfigValueByPath('namespace'); 549 | $externalProvider = $this->getExternalProvider(); 550 | $selectionsProvider = $this->getSelectionsProvider(); 551 | 552 | return [ 553 | 'data' => [ 554 | 'componentClassName' => Modal::class, 555 | 'config' => [ 556 | 'componentType' => Modal::NAME, 557 | 'component' => 'Magento_Ui/js/modal/modal-component', 558 | 'provider' => null, 559 | 'options' => [ 560 | 'buttons' => $this->getGenericModalButtons(), 561 | 'type' => 'slide', 562 | ], 563 | ], 564 | ], 565 | 'context' => $this->context, 566 | 'children' => [ 567 | $namespace => [ 568 | 'data' => [ 569 | 'config' => [ 570 | 'autoRender' => false, 571 | 'componentType' => 'insertListing', 572 | 'component' => 'Magento_Ui/js/form/components/insert-listing', 573 | 'firstLoad' => true, 574 | 'dataScope' => $namespace, 575 | 'externalProvider' => $externalProvider, 576 | 'selectionsProvider' => $selectionsProvider, 577 | 'ns' => $namespace, 578 | 'uniq_ns' => $this->generateUniqNamespace(), 579 | 'render_url' => $this->urlBuilder->getUrl('mui/index/render'), 580 | 'realTimeLink' => true, 581 | 'dataLinks' => [ 582 | 'imports' => false, 583 | 'exports' => true 584 | ], 585 | 'params' => [ 586 | 'namespace' => '${ $.ns }', 587 | 'js_modifier' => [ 588 | 'params' => $this->getJsModifierParams() 589 | ], 590 | '__disableTmpl' => ['namespace' => false], 591 | ], 592 | 'links' => [ 593 | 'value' => '${ $.provider }:${ $.uniq_ns }', 594 | '__disableTmpl' => ['value' => false] 595 | ], 596 | 'behaviourType' => 'simple', 597 | 'externalFilterMode' => true, 598 | ] 599 | ], 600 | 'context' => $this->context 601 | ] 602 | ] 603 | ]; 604 | } 605 | 606 | /** 607 | * Get js modifier params 608 | * 609 | * @return array 610 | * @throws LocalizedException 611 | */ 612 | public function getJsModifierParams(): array 613 | { 614 | $params = ['columns_name' => $this->getRequiredConfigValueByPath('columnsName')]; 615 | 616 | $limit = $this->getConfigByPath('limit/max'); 617 | if ($limit) { 618 | $params['limiter'] = [ 619 | 'selectionsProvider' => $this->getSelectionsProvider(), 620 | 'limit' => $limit 621 | ]; 622 | } 623 | 624 | return $params; 625 | } 626 | 627 | /** 628 | * Get generic modal buttons component 629 | * 630 | * @return array 631 | * @throws LocalizedException 632 | */ 633 | public function getGenericModalButtons(): array 634 | { 635 | $cancelButtonText = 636 | $this->getConfigByPath('modal/options/buttons/cancel/title') ?? __('Cancel'); 637 | $additionalCancelActions = 638 | $this->getConfigByPath('modal/options/buttons/cancel/additionalActions') ?? []; 639 | 640 | $saveButtonText = 641 | $this->getConfigByPath('modal/options/buttons/save/title') ?? __('Add Selected Items'); 642 | $additionalSaveActions = 643 | $this->getConfigByPath('modal/options/buttons/save/additionalActions') ?? []; 644 | 645 | return [ 646 | [ 647 | 'text' => $cancelButtonText, 648 | 'actions' => array_merge( 649 | [ 650 | 'closeModal' 651 | ], 652 | $additionalCancelActions 653 | ) 654 | ], 655 | [ 656 | 'text' => $saveButtonText, 657 | 'class' => 'action-primary', 658 | 'actions' => array_merge( 659 | [ 660 | [ 661 | 'targetName' => 'uniq_ns = ' . $this->generateUniqNamespace(), 662 | 'actionName' => 'save' 663 | ], 664 | 'closeModal' 665 | ], 666 | $additionalSaveActions 667 | ) 668 | ], 669 | ]; 670 | } 671 | 672 | /** 673 | * Get require config value by path 674 | * 675 | * @param string $path 676 | * @return array|mixed 677 | * @throws LocalizedException 678 | */ 679 | public function getRequiredConfigValueByPath(string $path) 680 | { 681 | $value = $this->getConfigByPath($path); 682 | if ($value === null) { 683 | throw new LocalizedException(__( 684 | 'The "%1" configuration parameter is required for the "%2" field.', 685 | 'config/' . $path, 686 | $this->getName() 687 | )); 688 | } 689 | 690 | return $value; 691 | } 692 | 693 | /** 694 | * Get config by path 695 | * 696 | * @param string $path 697 | * @return array|mixed|null 698 | */ 699 | public function getConfigByPath(string $path) 700 | { 701 | return $this->getDataByPath('config/' . $path); 702 | } 703 | 704 | /** 705 | * Generate uniq namespace 706 | * 707 | * @return string 708 | * @throws LocalizedException 709 | */ 710 | protected function generateUniqNamespace(): string 711 | { 712 | return sprintf( 713 | '%s_%s', 714 | $this->getRequiredConfigValueByPath('namespace'), 715 | $this->getName() 716 | ); 717 | } 718 | } 719 | -------------------------------------------------------------------------------- /Component/Widget/Form.php: -------------------------------------------------------------------------------- 1 | metadata = $metadata; 50 | $this->uiComponentFactory = $uiComponentFactory; 51 | 52 | $this->getDataProvider()->setMetadata($this->metadata); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function prepare(): void 59 | { 60 | $arguments = [ 61 | 'context' => $this->getContext(), 62 | 'data' => [ 63 | 'config' => [ 64 | 'component' => 'Grasch_AdminUi/js/widget/form/element/sync-field', 65 | 'template' => 'Grasch_AdminUi/widget/form/element/hidden', 66 | 'customInputName' => $this->metadata->getSyncFieldName() 67 | ], 68 | 'name' => 'sync_field' 69 | ] 70 | ]; 71 | 72 | $syncField = $this->uiComponentFactory->create( 73 | 'sync_field', 74 | BaseForm\Element\Hidden::NAME, 75 | $arguments 76 | ); 77 | $this->addComponent('sync_field', $syncField); 78 | $this->prepareChildComponent($syncField); 79 | 80 | parent::prepare(); 81 | } 82 | 83 | /** 84 | * Get dataSource data 85 | * 86 | * @return array 87 | */ 88 | public function getDataSourceData(): array 89 | { 90 | return ['data' => $this->getDataProvider()->getData()]; 91 | } 92 | 93 | /** 94 | * Get data provider 95 | * 96 | * @return DataProviderInterface 97 | */ 98 | public function getDataProvider(): DataProviderInterface 99 | { 100 | /** @var DataProviderInterface $dataProvider */ 101 | $dataProvider = $this->context->getDataProvider(); 102 | if (!$dataProvider instanceof DataProviderInterface) { 103 | throw new InvalidArgumentException( 104 | '$dataProvider must implement ' . DataProviderInterface::class 105 | ); 106 | } 107 | 108 | return $dataProvider; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /DataProvider/Widget/DataProvider.php: -------------------------------------------------------------------------------- 1 | metadata->getFormData(); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function setMetadata(MetadataInterface $metadata): void 53 | { 54 | $this->metadata = $metadata; 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function getMetadata(): MetadataInterface 61 | { 62 | return $this->metadata; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DataProvider/Widget/DataProviderInterface.php: -------------------------------------------------------------------------------- 1 | addChildren($children, $component, $component->getName()); 18 | 19 | return $children; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Model/Base64.php: -------------------------------------------------------------------------------- 1 | isValidBase64($value)) { 21 | $result = base64_decode($value, true); 22 | return ($result !== false) ? $result : null; 23 | } 24 | throw new LocalizedException(__('Data "%1" is incorrect.', $value)); 25 | } 26 | 27 | /** 28 | * Encode $value 29 | * 30 | * @param string $value 31 | * @return string 32 | * @phpcs:disable Magento2.Functions.DiscouragedFunction 33 | */ 34 | public function encode(string $value): string 35 | { 36 | return base64_encode($value); 37 | } 38 | 39 | /** 40 | * Validate base64 encoded value 41 | * 42 | * @param string $value 43 | * @return bool 44 | * @phpcs:disable Magento2.Functions.DiscouragedFunction 45 | */ 46 | public function isValidBase64(string $value): bool 47 | { 48 | $decodedValue = base64_decode($value, true); 49 | if ($decodedValue === false) { 50 | return false; 51 | } 52 | 53 | return base64_encode($decodedValue) === $value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Model/DecodeComponentValue.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 38 | $this->base64 = $base64; 39 | $this->logger = $logger; 40 | } 41 | 42 | /** 43 | * Decode component value 44 | * 45 | * @param string $value 46 | * @return mixed 47 | */ 48 | public function execute(string $value) 49 | { 50 | if (preg_match('/encodedComponentsData\|.*/', $value)) { 51 | $value = preg_replace('/encodedComponentsData\|/', '', $value); 52 | try { 53 | $value = $this->base64->decode($value); 54 | $value = $this->serializer->unserialize($value); 55 | } catch (LocalizedException $e) { 56 | $this->logger->error($e->getMessage()); 57 | }; 58 | } 59 | 60 | return $value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Model/JsConfiguration/ModifierComposite.php: -------------------------------------------------------------------------------- 1 | modifiers = $modifiers; 22 | } 23 | 24 | /** 25 | * Run modifiers 26 | * 27 | * @param array $configuration 28 | * @param ContextInterface $context 29 | * @param array $params 30 | * @return array 31 | */ 32 | public function execute( 33 | array $configuration, 34 | ContextInterface $context, 35 | array $params = [] 36 | ): array { 37 | foreach ($this->modifiers as $modifier) { 38 | $configuration = $modifier->execute( 39 | $configuration, 40 | $context, 41 | $params 42 | ); 43 | } 44 | 45 | return $configuration; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Model/JsConfiguration/ModifierInterface.php: -------------------------------------------------------------------------------- 1 | _arrayManager = $arrayManager; 23 | } 24 | 25 | /** 26 | * Get path to main component 27 | * 28 | * @param ContextInterface $context 29 | * @return string 30 | */ 31 | public function getPathToMainComponent(ContextInterface $context): string 32 | { 33 | return sprintf( 34 | 'components/%s/children/%s', 35 | $context->getNamespace(), 36 | $context->getNamespace() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Model/JsConfiguration/Modifiers/ListingComponentModifier.php: -------------------------------------------------------------------------------- 1 | getPathToMainComponent($context), 31 | $params['columns_name'] 32 | ); 33 | 34 | return $this->_arrayManager->merge( 35 | $path, 36 | $configuration, 37 | $this->getConfig() 38 | ); 39 | } 40 | 41 | /** 42 | * Get config 43 | * 44 | * @return \false[][] 45 | */ 46 | private function getConfig(): array 47 | { 48 | return [ 49 | 'editorConfig' => [ 50 | 'enabled' => false, 51 | ], 52 | 'dndConfig' => [ 53 | 'enabled' => false 54 | ], 55 | 'resizeConfig' => [ 56 | 'enabled' => false 57 | ], 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Model/JsConfiguration/Modifiers/MultiselectComponentModifier.php: -------------------------------------------------------------------------------- 1 | _arrayManager->merge( 35 | $path, 36 | $configuration, 37 | [ 38 | 'limit' => $params['limiter']['limit'], 39 | 'component' => 'Grasch_AdminUi/js/view/grid/columns/multiselect-with-limit' 40 | ] 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Model/UiComponentGenerator.php: -------------------------------------------------------------------------------- 1 | . 26 | */ 27 | declare(strict_types=1); 28 | 29 | namespace Grasch\AdminUi\Model; 30 | 31 | use Magento\Framework\ObjectManagerInterface; 32 | use Magento\Framework\View\Element\UiComponent\ContextInterface; 33 | use Magento\Framework\View\Element\UiComponentInterface; 34 | use Magento\Ui\Component\Container; 35 | 36 | class UiComponentGenerator 37 | { 38 | /** 39 | * @var ObjectManagerInterface 40 | */ 41 | private ObjectManagerInterface $objectManager; 42 | 43 | /** 44 | * @param ObjectManagerInterface $objectManager 45 | */ 46 | public function __construct( 47 | ObjectManagerInterface $objectManager 48 | ) { 49 | $this->objectManager = $objectManager; 50 | } 51 | 52 | /** 53 | * Generate child components from array 54 | * 55 | * @param UiComponentInterface $parent 56 | * @param array $children 57 | * @return UiComponentInterface 58 | */ 59 | public function generateChildComponentsFromArray( 60 | UiComponentInterface $parent, 61 | array $children 62 | ): UiComponentInterface { 63 | foreach ($children as $childName => $child) { 64 | $component = $this->createUiComponent( 65 | $childName, 66 | $child['data'], 67 | $child['context'] 68 | ); 69 | if (isset($child['children'])) { 70 | $this->generateChildComponentsFromArray( 71 | $component, 72 | $child['children'] 73 | ); 74 | } 75 | $parent->addComponent($childName, $component); 76 | } 77 | 78 | return $parent; 79 | } 80 | 81 | /** 82 | * Create ui component 83 | * 84 | * @param string $name 85 | * @param array $data 86 | * @param ContextInterface $context 87 | * @return UiComponentInterface 88 | */ 89 | public function createUiComponent( 90 | string $name, 91 | array $data, 92 | ContextInterface $context 93 | ): UiComponentInterface { 94 | $data['name'] = $name; 95 | 96 | return $this->objectManager->create( 97 | $data['componentClassName'] ?? Container::class, 98 | [ 99 | 'data' => $data, 100 | 'context' => $context 101 | ] 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Model/Widget/AdditionalPreparer/PreparerInterface.php: -------------------------------------------------------------------------------- 1 | base64 = $base64; 31 | $this->pageBuilderState = $pageBuilderState; 32 | } 33 | 34 | /** 35 | * Prepare wysiwyg 36 | * 37 | * @param UiComponentInterface $component 38 | * @return mixed|void 39 | */ 40 | public function prepare(UiComponentInterface $component) 41 | { 42 | $config = $component->getData('config'); 43 | 44 | $config['content'] = $this->base64->encode($config['content']); 45 | 46 | $wysiwygConfigData = isset($config['wysiwygConfigData']) ? $config['wysiwygConfigData'] : []; 47 | $isEnablePageBuilder = isset($wysiwygConfigData['is_pagebuilder_enabled']) 48 | && !$wysiwygConfigData['is_pagebuilder_enabled'] 49 | || false; 50 | if (!$this->pageBuilderState->isPageBuilderInUse($isEnablePageBuilder)) { 51 | $config['component'] = 'Grasch_AdminUi/js/widget/form/element/page-builder/wysiwyg'; 52 | } else { 53 | $config['component'] = 'Grasch_AdminUi/js/widget/form/element/wysiwyg'; 54 | } 55 | 56 | $component->setData('config', $config); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Model/Widget/Metadata.php: -------------------------------------------------------------------------------- 1 | getData(self::FORM_DATA); 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function setFormData(array $data): void 22 | { 23 | $this->setData(self::FORM_DATA, $data); 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function getSyncFieldName(): string 30 | { 31 | return $this->getData(self::SYNC_FIELD_NAME); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function setSyncFieldName(string $name): void 38 | { 39 | $this->setData(self::SYNC_FIELD_NAME, $name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Model/Widget/MetadataInterface.php: -------------------------------------------------------------------------------- 1 | additionalPreparers = $additionalPreparers; 24 | } 25 | 26 | /** 27 | * Prepare ui component 28 | * 29 | * @param UiComponentInterface $component 30 | * @return void 31 | */ 32 | public function execute(UiComponentInterface $component): void 33 | { 34 | $childComponents = $component->getChildComponents(); 35 | if (!empty($childComponents)) { 36 | foreach ($childComponents as $child) { 37 | $this->execute($child); 38 | } 39 | } 40 | $component->prepare(); 41 | 42 | $componentName = !empty($component->getData()['formElement']) 43 | ? $component->getData()['formElement'] 44 | : $component->getComponentName(); 45 | 46 | if (isset($this->additionalPreparers[$componentName])) { 47 | /** @var PreparerInterface $preparer */ 48 | foreach ($this->additionalPreparers[$componentName] as $preparer) { 49 | if (!$preparer instanceof PreparerInterface) { 50 | throw new InvalidArgumentException( 51 | '$preparer must implement ' . PreparerInterface::class 52 | ); 53 | } 54 | $preparer->prepare($component); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Model/Widget/UiComponentFactory.php: -------------------------------------------------------------------------------- 1 | getComponentName() !== Form::NAME) { 31 | return $resultComponent; 32 | } 33 | 34 | return $this->objectManager->create( 35 | WidgetForm::class, 36 | [ 37 | 'context' => $resultComponent->getContext(), 38 | 'components' => $resultComponent->getChildComponents(), 39 | 'data' => $resultComponent->getData(), 40 | 'metadata' => $arguments['metadata'] 41 | ] 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Plugin/Magento/Framework/View/Layout/Generic/ModifyJsConfiguration.php: -------------------------------------------------------------------------------- 1 | request = $request; 32 | $this->modifier = $modifier; 33 | } 34 | 35 | /** 36 | * Modify ui components 37 | * 38 | * @param Generic $subject 39 | * @param array $result 40 | * @param UiComponentInterface $component 41 | * @return array 42 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 43 | */ 44 | public function afterBuild( 45 | Generic $subject, 46 | array $result, 47 | UiComponentInterface $component 48 | ): array { 49 | $modifier = $this->request->getParam('js_modifier'); 50 | if (!$modifier) { 51 | return $result; 52 | } 53 | 54 | return $this->modifier->execute( 55 | $result, 56 | $component->getContext(), 57 | $modifier['params'] ?? [] 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Plugin/Magento/Widget/Block/Adminhtml/Widget/Options/DecodeComponentValues.php: -------------------------------------------------------------------------------- 1 | decodeComponentValue = $decodeComponentValue; 23 | } 24 | 25 | /** 26 | * Decode component value 27 | * 28 | * @param Options $subject 29 | * @param mixed $result 30 | * @param string $key 31 | * @return array|mixed 32 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 33 | */ 34 | public function afterGetData( 35 | Options $subject, 36 | $result, 37 | string $key 38 | ) { 39 | if ($key !== 'widget_values') { 40 | return $result; 41 | } 42 | 43 | if (!$result || !is_array($result)) { 44 | return $result; 45 | } 46 | 47 | foreach ($result as &$value) { 48 | $value = $this->decodeComponentValue->execute($value); 49 | } 50 | 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | grasch/magento-2-admin-ui 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Highlight features for Magento 2 Admin UI 9 | - Use ui components in widgets. 10 | - New ui components. 11 | 12 | ## How to install Magento 2 Admin UI 13 | 14 | ### ✓ Install via composer (recommend) 15 | 16 | Run the following commands in Magento 2 root folder: 17 | 18 | ``` 19 | composer require grasch/module-admin-ui 20 | php bin/magento setup:upgrade 21 | php bin/magento setup:static-content:deploy 22 | ``` 23 | ### ✓ Install via downloading 24 | 25 | Download and copy files into `app/code/Grasch/AdminUi` and run the following commands: 26 | ``` 27 | php bin/magento setup:upgrade 28 | php bin/magento setup:static-content:deploy 29 | ``` 30 | 31 | ## Usage Documentation 32 | - UI Components 33 | - [Entities Selector](docs/ui-components/entities-selector/README.md) 34 | - Widgets 35 | - [Using UI Components Inside Widgets](docs/widgets/using-ui-components/README.md) 36 | 37 | ## The MIT License 38 | [](https://opensource.org/licenses/MIT) 39 | 40 | -------------------------------------------------------------------------------- /ViewModel/PageBuilder.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | $this->moduleManager = $moduleManager; 31 | } 32 | 33 | /** 34 | * @return bool 35 | */ 36 | public function isPageBuilderEnabled(): bool 37 | { 38 | return $this->moduleManager->isEnabled('Magento_PageBuilder') 39 | && $this->config->isEnabled(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grasch/module-admin-ui", 3 | "description": "N/A", 4 | "type": "magento2-module", 5 | "require": { 6 | "php": ">=7.4.0", 7 | "magento/framework": "103.0.*", 8 | "magento/module-ui": "101.2.*" 9 | }, 10 | "license": [ 11 | "MIT" 12 | ], 13 | "autoload": { 14 | "files": [ 15 | "registration.php" 16 | ], 17 | "psr-4": { 18 | "Grasch\\AdminUi\\": "" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/ui-components/entities-selector/README.md: -------------------------------------------------------------------------------- 1 | Entities Selector UI Component 2 | 3 | The Entities Selector makes it easy to create component with the same functionality as 'Related Products, Up-Sells, and Cross-Sells' on the product form. 4 | 5 |  6 | 7 | ## What are the benefits? 8 | - You don't need to write PHP code. Add only the Entities Selector component to the XML. 9 | - You can use any listings (product_listing, cms_page_listing, customer_listing, etc). 10 | - Easy to configure. 11 | 12 | ## Options 13 | ### General options 14 | Options | Description | Type | Default | Is Required 15 | --- |-----------------------------------------------------------|---------|---------| --- 16 | `namespace` | The name of the XML file with listing | string | `null` | `true` 17 | `columnsName` | The name of the columns component in the listing | string | `null` | `true` 18 | `selectionsColumnName` | The name of the selectionsColumn component in the listing | string | `null` | `true` 19 | `label` | Caption for an component | string | `null` | `true` 20 | `grid/columns` | Array of columns | array | `[]` | `true` 21 | `grid/columns/{columnName}` | Column configuration array | array | `[]` | `true` 22 | `grid/dndEnabled` | Enable/Disable drag and drop functionality | boolean | `true` | `false` 23 | `limit/min` | Minimum limit | number | `null` | `false` 24 | `limit/max` | Maximum limit | number | `null` | `false` 25 | 26 | ### Column configuration options 27 | Options | Description | Type | Default | Is Required 28 | --- |-----------------------------------|---------| --- | --- 29 | `original_name` | The name of the column in the grid | string | `null` | `true` 30 | `type` | Type (text, thumbnail) | string | `null` | `true` 31 | `label` | Heading for a column | string | `null` | `true` 32 | `fit` | Сolumn width | boolean | `null` | `true` 33 | `sortOrder` | Column sort order | number | `null` | `true` 34 | 35 | 36 | ## Examples 37 | 38 | ### 1. Add component to Sales Rule form. (Listing: cms_page_listing). 39 | 40 | ```xml 41 | 42 | ... 43 | 44 | ... 45 | 46 | 47 | 48 | Cms Pages 49 | cms_page_listing 50 | cms_page_columns 51 | ids 52 | 53 | 54 | 55 | page_id 56 | text 57 | ID 58 | 10 59 | false 60 | 61 | 62 | identifier 63 | text 64 | identifier 65 | 20 66 | false 67 | 68 | 69 | is_active 70 | text 71 | Is Active 72 | 15 73 | true 74 | 75 | 76 | 77 | 78 | 79 | 80 | ... 81 | 82 | ... 83 | 84 | ``` 85 | 86 | ### 2. Add component to Sales Rule form. (Listing: product_listing, Min: 2, Max: 4). 87 | ```xml 88 | 89 | ... 90 | 91 | ... 92 | 93 | 94 | 95 | 96 | 2 97 | 4 98 | 99 | Products 100 | product_listing 101 | product_columns 102 | ids 103 | 104 | 105 | 106 | entity_id 107 | text 108 | ID 109 | 10 110 | true 111 | 112 | 113 | sku 114 | text 115 | SKU 116 | 15 117 | false 118 | 119 | 120 | name 121 | text 122 | Name 123 | 20 124 | false 125 | 126 | 127 | thumbnail_src 128 | thumbnail 129 | Thumbnail 130 | 16 131 | true 132 | 133 | 134 | 135 | 136 | 137 | 138 | ... 139 | 140 | ... 141 | 142 | ``` 143 | ### Results 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/ui-components/entities-selector/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/ui-components/entities-selector/demo.gif -------------------------------------------------------------------------------- /docs/ui-components/entities-selector/demo_screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/ui-components/entities-selector/demo_screen_1.png -------------------------------------------------------------------------------- /docs/ui-components/entities-selector/demo_screen_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/ui-components/entities-selector/demo_screen_2.png -------------------------------------------------------------------------------- /docs/ui-components/entities-selector/demo_screen_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/ui-components/entities-selector/demo_screen_3.png -------------------------------------------------------------------------------- /docs/widgets/using-ui-components/README.md: -------------------------------------------------------------------------------- 1 | Using UI Components Inside Widgets 2 | 3 | ## What are the benefits? 4 | - You can use UI components inside widgets instead of limited functionality of widgets. 5 | - You don't need to worry about data storage as the data is automatically stored inside the widgets. 6 | - Easy to configure. 7 | 8 |  9 | 10 | ## Example 11 | You can see an example of a widget with most of the available UI components. The widget is called "[Example Widget with UI Components](../../../etc/widget.xml)". 12 | 13 | ## How to use it? 14 | - Create [app/code/Vendor/Module/etc/widget.xml](../../../etc/widget.xml) file inside your module. 15 | - Create a widget class that will extend from ```Grasch\AdminUi\Block\Widget\AbstractWidget```. 16 | - You have to add only one parameter ```block``` with class ```Grasch\AdminUi\Block\Adminhtml\Widget\Ui\Components```. 17 | - ```namespace``` is the name of your form.xml file. 18 | ```xml 19 | 20 | 21 | 22 | 23 | widget_example_form 24 | 25 | 26 | 27 | 28 | ``` 29 | - Create [app/code/Vendor/Module/view/adminhtml/ui_component/form.xml](../../../view/adminhtml/ui_component/widget_example_form.xml) file inside your module. Add the UI Components that you need here. 30 | - Use this class ```Grasch\AdminUi\DataProvider\Widget\DataProvider``` as ```dataProvider``` for your form. 31 | - Get data from a widget. 32 | ```php 33 | /** 34 | * @return string 35 | */ 36 | protected function _toHtml(): string 37 | { 38 | $data = $this->getData('component_data'); 39 | print_r($data); 40 | 41 | return ''; 42 | } 43 | ``` 44 |  45 | -------------------------------------------------------------------------------- /docs/widgets/using-ui-components/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/widgets/using-ui-components/demo.gif -------------------------------------------------------------------------------- /docs/widgets/using-ui-components/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graschik/magento-2-admin-ui/e3aaaedc8894eda50de11a58ecee8d8d2235ba2b/docs/widgets/using-ui-components/result.png -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Grasch\AdminUi\Layout\Generic 9 | templates/layout/generic 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Grasch\AdminUi\Model\JsConfiguration\Modifiers\ListingComponentModifier 23 | Grasch\AdminUi\Model\JsConfiguration\Modifiers\MultiselectComponentModifier 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Grasch\AdminUi\Model\Widget\AdditionalPreparer\WysiwygPreparer 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /etc/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | Example Of Widget With Ui Components 7 | Example Of Widget With Ui Components 8 | 9 | 10 | 11 | 12 | widget_example_form 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Grasch_AdminUi::widget/form/container.phtml 8 | \Grasch\AdminUi\ViewModel\PageBuilder 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/adminhtml/layout/adminhtml_widget_loadoptions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | \Grasch\AdminUi\ViewModel\PageBuilder 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /view/adminhtml/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /view/adminhtml/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | map: { 3 | '*': { 4 | 'mage/adminhtml/wysiwyg/widget': 'Grasch_AdminUi/js/wysiwyg/widget' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /view/adminhtml/templates/widget/form/container.phtml: -------------------------------------------------------------------------------- 1 | 6 | = /* @noEscape */ $block->getFormInitScripts() ?> 7 | getButtonsHtml('header')): ?> 8 | getUiId('content-header') ?>> 9 | = $block->getButtonsHtml('header') ?> 10 | 11 | 12 | getButtonsHtml('toolbar')): ?> 13 | 14 | 15 | 16 | = $block->getButtonsHtml('toolbar') ?> 17 | 18 | 19 | 20 | 21 | = $block->getFormHtml() ?> 22 | hasFooterButtons()): ?> 23 | 26 | 27 | 28 | 31 | 125 | 126 | = /* @noEscape */ $block->getFormScripts() ?> 127 | -------------------------------------------------------------------------------- /view/adminhtml/templates/widget/initialize-ui-components.phtml: -------------------------------------------------------------------------------- 1 | getPageBuilder(); 5 | ?> 6 | 7 | 10 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/widget_example_form.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | widget_example_form.widget_example_form_data_source 6 | 7 | 8 | 9 | widget_example_form 10 | data 11 | 12 | widget_example_form.widget_example_form_data_source 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | General Fieldset 21 | true 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 2 29 | 4 30 | 31 | Products 32 | product_listing 33 | product_columns 34 | ids 35 | 36 | 37 | 38 | entity_id 39 | text 40 | ID 41 | 10 42 | true 43 | 44 | 45 | sku 46 | text 47 | SKU 48 | 15 49 | false 50 | 51 | 52 | name 53 | text 54 | Name 55 | 20 56 | false 57 | 58 | 59 | thumbnail_src 60 | thumbnail 61 | Thumbnail 62 | 16 63 | true 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 1 74 | 75 | 76 | 77 | boolean 78 | Enable 79 | 80 | 81 | 82 | 83 | 84 | 0 85 | 1 86 | 87 | toggle 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 1 96 | 97 | 98 | 99 | boolean 100 | Enable 101 | 102 | 103 | 104 | 105 | 106 | 0 107 | 1 108 | 109 | checkbox 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 1 118 | 119 | 120 | 121 | boolean 122 | Enable 123 | 124 | 125 | 126 | 127 | 128 | 0 129 | 1 130 | 131 | radio 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Additional information 140 | 141 | 142 | 143 | checkboxset 144 | Checkboxset 145 | 146 | 147 | 1 148 | Option #1 149 | 150 | 151 | 2 152 | Option #2 153 | 154 | 155 | 3 156 | Option #3 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | true 165 | 166 | text 167 | Text Field 168 | title 169 | 170 | 171 | 172 | 173 | 174 | 0 175 | 176 | 177 | 178 | 179 | true 180 | 181 | int 182 | 183 | https://docs.magento.com/user-guide/configuration/scope.html 184 | What is this? 185 | 186 | Store View 187 | store_id 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | textarea 201 | 15 202 | 5 203 | Textarea 204 | text 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | TARGET_NAME 215 | ACTION_NAME 216 | 217 | 218 | 219 | 220 | 221 | false 222 | 223 | 224 | 225 | 226 | 227 | Color 228 | ui/form/element/color-picker 229 | rgb 230 | full 231 | colors_filter 232 | 233 | 234 | 235 | 236 | 237 | true 238 | 239 | text 240 | date 241 | Date 242 | true 243 | 244 | 245 | 246 | 247 | Some notice. 248 | Image Uploader 249 | imageUploader 250 | image_uploader 251 | 252 | 253 | 254 | 255 | jpg jpeg gif png 256 | 2097152 257 | 258 | path/to/save 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | actionCancel 268 | 269 | 270 | 271 | Cancel 272 | action-secondary 273 | 274 | actionCancel 275 | 276 | 277 | 278 | Clear 279 | action-secondary 280 | 281 | 282 | ${ $.name }.testField 283 | clear 284 | 285 | 286 | 287 | 288 | Done 289 | action-primary 290 | 291 | actionDone 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | text 305 | Test Field 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | Open modal 314 | 315 | 316 | ${ $.parentName}.test_modal 317 | openModal 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Magento\Ui\Model\UrlInput\LinksConfigProvider 327 | 328 | 329 | 330 | Url Input 331 | url_input 332 | 333 | 334 | 335 | 336 | 337 | 338 | 100px 339 | true 340 | true 341 | true 342 | true 343 | 344 | 345 | 346 | 347 | Content 1 348 | wysiwyg 349 | 350 | 351 | 352 | 353 | 8 354 | true 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 100px 364 | true 365 | true 366 | true 367 | true 368 | 369 | 370 | 371 | 372 | Content 2 373 | wysiwyg_2 374 | 375 | 376 | 377 | 378 | Dynamic Rows 379 | dynamic_rows 380 | Add Record 381 | 382 | true 383 | 384 | dynamicRows 385 | 386 | 387 | 388 | 389 | true 390 | true 391 | container 392 | 393 | 394 | 395 | 396 | 397 | false 398 | 399 | 400 | 401 | 402 | true 403 | 404 | text 405 | Field #1 406 | 407 | 408 | 409 | 410 | 411 | false 412 | 413 | 414 | 415 | 416 | true 417 | 418 | text 419 | Field #2 420 | 421 | 422 | 423 | 424 | false 425 | 426 | 427 | 428 | 429 | 430 | 431 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/model/messageList.js: -------------------------------------------------------------------------------- 1 | define([ 2 | './messages' 3 | ], function (Messages) { 4 | 'use strict'; 5 | 6 | return new Messages(); 7 | }); 8 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/model/messages.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'ko', 3 | 'uiClass' 4 | ], function (ko, Class) { 5 | 'use strict'; 6 | 7 | return Class.extend({ 8 | /** @inheritdoc */ 9 | initialize: function () { 10 | this._super() 11 | .initObservable(); 12 | 13 | return this; 14 | }, 15 | 16 | /** @inheritdoc */ 17 | initObservable: function () { 18 | this.errorMessages = ko.observableArray([]); 19 | this.successMessages = ko.observableArray([]); 20 | this.noticeMessages = ko.observableArray([]); 21 | this.warningMessages = ko.observableArray([]); 22 | this.messagesConfig = ko.observableArray([ 23 | {type: 'error', messages: this.errorMessages}, 24 | {type: 'success', messages: this.successMessages}, 25 | {type: 'warning', messages: this.warningMessages}, 26 | {type: 'notice', messages: this.noticeMessages}, 27 | ]); 28 | 29 | return this; 30 | }, 31 | 32 | /** 33 | * Add message to list. 34 | * @param {Object} messageObj 35 | * @param {Object} type 36 | * @returns {Boolean} 37 | */ 38 | add: function (messageObj, type) { 39 | var expr = /([%])\w+/g, 40 | message; 41 | 42 | if (!messageObj.hasOwnProperty('parameters')) { 43 | this.clear(); 44 | type.push(messageObj.message); 45 | 46 | return true; 47 | } 48 | message = messageObj.message.replace(expr, function (varName) { 49 | varName = varName.substr(1); 50 | 51 | if (!isNaN(varName)) { 52 | varName--; 53 | } 54 | 55 | if (messageObj.parameters.hasOwnProperty(varName)) { 56 | return messageObj.parameters[varName]; 57 | } 58 | 59 | return messageObj.parameters.shift(); 60 | }); 61 | this.clear(); 62 | type.push(message); 63 | 64 | return true; 65 | }, 66 | 67 | /** 68 | * Add success message. 69 | * 70 | * @param {Object} message 71 | * @return {*|Boolean} 72 | */ 73 | addSuccessMessage: function (message) { 74 | return this.add(message, this.successMessages); 75 | }, 76 | 77 | /** 78 | * Add error message. 79 | * 80 | * @param {Object} message 81 | * @return {*|Boolean} 82 | */ 83 | addErrorMessage: function (message) { 84 | return this.add(message, this.errorMessages); 85 | }, 86 | 87 | /** 88 | * Add notice message. 89 | * 90 | * @param {Object} message 91 | * @return {*|Boolean} 92 | */ 93 | addNoticeMessage: function (message) { 94 | return this.add(message, this.noticeMessages); 95 | }, 96 | 97 | /** 98 | * Add warning message. 99 | * 100 | * @param {Object} message 101 | * @return {*|Boolean} 102 | */ 103 | addWarningMessage: function (message) { 104 | return this.add(message, this.warningMessages); 105 | }, 106 | 107 | /** 108 | * Get error messages. 109 | * 110 | * @return {Array} 111 | */ 112 | getErrorMessages: function () { 113 | return this.errorMessages; 114 | }, 115 | 116 | /** 117 | * Get success messages. 118 | * 119 | * @return {Array} 120 | */ 121 | getSuccessMessages: function () { 122 | return this.successMessages; 123 | }, 124 | 125 | /** 126 | * Get notice messages. 127 | * 128 | * @return {Array} 129 | */ 130 | getNoticeMessages: function () { 131 | return this.noticeMessages; 132 | }, 133 | 134 | /** 135 | * Get warning messages. 136 | * 137 | * @return {Array} 138 | */ 139 | getWarningMessages: function () { 140 | return this.warningMessages; 141 | }, 142 | 143 | /** 144 | * Checks if an instance has stored messages. 145 | * 146 | * @return {Boolean} 147 | */ 148 | hasMessages: function () { 149 | return this.errorMessages().length > 0 150 | || this.successMessages().length > 0 151 | || this.noticeMessages().length > 0 152 | || this.warningMessages().length > 0; 153 | }, 154 | 155 | /** 156 | * Removes stored messages. 157 | */ 158 | clear: function () { 159 | this.errorMessages.removeAll(); 160 | this.successMessages.removeAll(); 161 | this.noticeMessages.removeAll(); 162 | this.warningMessages.removeAll(); 163 | } 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/utils/base64.js: -------------------------------------------------------------------------------- 1 | !function(t,n){var r,e;"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define('base64', n):(r=t.Base64,(e=n()).noConflict=function(){return t.Base64=r,e},t.Meteor&&(Base64=e),t.Base64=e)}("undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:this,(function(){"use strict";var t,n="3.7.2",r="function"==typeof atob,e="function"==typeof btoa,o="function"==typeof Buffer,u="function"==typeof TextDecoder?new TextDecoder:void 0,i="function"==typeof TextEncoder?new TextEncoder:void 0,f=Array.prototype.slice.call("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="),c=(t={},f.forEach((function(n,r){return t[n]=r})),t),a=/^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/,d=String.fromCharCode.bind(String),s="function"==typeof Uint8Array.from?Uint8Array.from.bind(Uint8Array):function(t,n){return void 0===n&&(n=function(t){return t}),new Uint8Array(Array.prototype.slice.call(t,0).map(n))},l=function(t){return t.replace(/=/g,"").replace(/[+\/]/g,(function(t){return"+"==t?"-":"_"}))},h=function(t){return t.replace(/[^A-Za-z0-9\+\/]/g,"")},p=function(t){for(var n,r,e,o,u="",i=t.length%3,c=0;c255||(e=t.charCodeAt(c++))>255||(o=t.charCodeAt(c++))>255)throw new TypeError("invalid character found");u+=f[(n=r<<16|e<<8|o)>>18&63]+f[n>>12&63]+f[n>>6&63]+f[63&n]}return i?u.slice(0,i-3)+"===".substring(i):u},y=e?function(t){return btoa(t)}:o?function(t){return Buffer.from(t,"binary").toString("base64")}:p,A=o?function(t){return Buffer.from(t).toString("base64")}:function(t){for(var n=[],r=0,e=t.length;r>>6)+d(128|63&n):d(224|n>>>12&15)+d(128|n>>>6&63)+d(128|63&n);var n=65536+1024*(t.charCodeAt(0)-55296)+(t.charCodeAt(1)-56320);return d(240|n>>>18&7)+d(128|n>>>12&63)+d(128|n>>>6&63)+d(128|63&n)},B=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g,x=function(t){return t.replace(B,g)},C=o?function(t){return Buffer.from(t,"utf8").toString("base64")}:i?function(t){return A(i.encode(t))}:function(t){return y(x(t))},m=function(t,n){return void 0===n&&(n=!1),n?l(C(t)):C(t)},v=function(t){return m(t,!0)},U=/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g,F=function(t){switch(t.length){case 4:var n=((7&t.charCodeAt(0))<<18|(63&t.charCodeAt(1))<<12|(63&t.charCodeAt(2))<<6|63&t.charCodeAt(3))-65536;return d(55296+(n>>>10))+d(56320+(1023&n));case 3:return d((15&t.charCodeAt(0))<<12|(63&t.charCodeAt(1))<<6|63&t.charCodeAt(2));default:return d((31&t.charCodeAt(0))<<6|63&t.charCodeAt(1))}},w=function(t){return t.replace(U,F)},S=function(t){if(t=t.replace(/\s+/g,""),!a.test(t))throw new TypeError("malformed base64.");t+="==".slice(2-(3&t.length));for(var n,r,e,o="",u=0;u>16&255):64===e?d(n>>16&255,n>>8&255):d(n>>16&255,n>>8&255,255&n);return o},E=r?function(t){return atob(h(t))}:o?function(t){return Buffer.from(t,"base64").toString("binary")}:S,D=o?function(t){return s(Buffer.from(t,"base64"))}:function(t){return s(E(t),(function(t){return t.charCodeAt(0)}))},R=function(t){return D(T(t))},z=o?function(t){return Buffer.from(t,"base64").toString("utf8")}:u?function(t){return u.decode(D(t))}:function(t){return w(E(t))},T=function(t){return h(t.replace(/[-_]/g,(function(t){return"-"==t?"+":"/"})))},Z=function(t){return z(T(t))},j=function(t){return{value:t,enumerable:!1,writable:!0,configurable:!0}},I=function(){var t=function(t,n){return Object.defineProperty(String.prototype,t,j(n))};t("fromBase64",(function(){return Z(this)})),t("toBase64",(function(t){return m(this,t)})),t("toBase64URI",(function(){return m(this,!0)})),t("toBase64URL",(function(){return m(this,!0)})),t("toUint8Array",(function(){return R(this)}))},O=function(){var t=function(t,n){return Object.defineProperty(Uint8Array.prototype,t,j(n))};t("toBase64",(function(t){return b(this,t)})),t("toBase64URI",(function(){return b(this,!0)})),t("toBase64URL",(function(){return b(this,!0)}))},P={version:n,VERSION:"3.7.2",atob:E,atobPolyfill:S,btoa:y,btoaPolyfill:p,fromBase64:Z,toBase64:m,encode:m,encodeURI:v,encodeURL:v,utob:x,btou:w,decode:Z,isValid:function(t){if("string"!=typeof t)return!1;var n=t.replace(/\s+/g,"").replace(/={0,2}$/,"");return!/[^\s0-9a-zA-Z\+/]/.test(n)||!/[^\s0-9a-zA-Z\-_]/.test(n)},fromUint8Array:b,toUint8Array:R,extendString:I,extendUint8Array:O,extendBuiltins:function(){I(),O()},Base64:{}};return Object.keys(P).forEach((function(t){return P.Base64[t]=P[t]})),P})); 2 | //# sourceMappingURL=/sm/79de78edcfa94236e4c8354f91262971e185c3633bb865b6fc17942e93a40207.map 3 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/view/components/entities-selector.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'ko', 3 | 'jquery', 4 | 'uiComponent', 5 | 'uiLayout', 6 | 'Grasch_AdminUi/js/model/messages', 7 | 'mage/translate' 8 | ], function (ko, $, Component, layout, Messages, $t) { 9 | 'use strict'; 10 | 11 | return Component.extend({ 12 | defaults: { 13 | listens: { 14 | isHidden: 'onHiddenChange', 15 | '${ $.provider }:${ $.customScope ? $.customScope + "." : ""}data.validate': 'validate', 16 | '${ $.name + "." + $.index }:recordData': 'validate' 17 | }, 18 | modules: { 19 | grid: '${ $.name + "." + $.index }' 20 | }, 21 | minMessage: $t('You must select at least %limit items.'), 22 | maxMessage: $t('You can\'t select more than %limit items.') 23 | }, 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | initialize: function () { 29 | this._super(); 30 | 31 | this.messageContainer = new Messages(); 32 | layout([{ 33 | parent: this.name + '.button_set', 34 | name: 'messages', 35 | component: 'Grasch_AdminUi/js/view/messages', 36 | config: { 37 | displayArea: 'messages', 38 | messageContainer: this.messageContainer, 39 | sortOrder: 0, 40 | isAlwaysVisible: true, 41 | removeOnClick: false 42 | } 43 | }]); 44 | 45 | return this; 46 | }, 47 | 48 | /** 49 | * @return {Object} 50 | */ 51 | validate: function () { 52 | if (!this.limit) { 53 | return true; 54 | } 55 | 56 | var isValid = true; 57 | 58 | if (!this.validateMinLimit()) { 59 | this.messageContainer.addWarningMessage({ 60 | message: this.minMessage, 61 | parameters: { 62 | limit: this.limit.min 63 | } 64 | }); 65 | isValid = false; 66 | } else if (!this.validateMaxLimit()) { 67 | this.messageContainer.addWarningMessage({ 68 | message: this.maxMessage, 69 | parameters: { 70 | limit: this.limit.max 71 | } 72 | }); 73 | isValid = false; 74 | } 75 | 76 | if (this.source && !isValid) { 77 | this.source.set('params.invalid', true); 78 | } 79 | if (isValid) { 80 | this.messageContainer.clear(); 81 | } 82 | 83 | return { 84 | valid: isValid, 85 | target: this 86 | }; 87 | }, 88 | 89 | /** 90 | * @return {Boolean} 91 | */ 92 | validateMaxLimit: function () { 93 | if (!this.limit || !this.limit.max) { 94 | return true; 95 | } 96 | 97 | return this.grid().recordData().length <= this.limit.max; 98 | }, 99 | 100 | /** 101 | * @return {Boolean} 102 | */ 103 | validateMinLimit: function () { 104 | if (!this.limit || !this.limit.min) { 105 | return true; 106 | } 107 | 108 | return this.grid().recordData().length >= this.limit.min; 109 | } 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/view/components/entities-selector/insert-listing/renderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @api 3 | */ 4 | define([ 5 | 'underscore', 6 | 'uiElement', 7 | 'uiRegistry' 8 | ], function (_, Element, registry) { 9 | 'use strict'; 10 | 11 | return Element.extend({ 12 | render: function () { 13 | var 14 | self = this, 15 | insertListing, 16 | componentsWithTheSameNs; 17 | 18 | insertListing = registry.get(self.insertListing); 19 | 20 | componentsWithTheSameNs = registry.filter(function (component) { 21 | return component.externalListingName == insertListing.ns + '.' + insertListing.ns; 22 | }); 23 | if (componentsWithTheSameNs.length > 1) { 24 | insertListing.isRendered = true; 25 | insertListing.destroyInserted(); 26 | 27 | registry.get(self.selectionsProvider, function (selections) { 28 | if (insertListing.firstLoad) { 29 | self.listens[selections.name + ':rows'] = 'updateListingFiltersForFirstLoad'; 30 | self.initLinks(); 31 | } else { 32 | var 33 | grid = registry.get(self.grid), 34 | selected = []; 35 | 36 | _.each(grid.recordData(), function (row) { 37 | selected.push(row[grid.identificationDRProperty]); 38 | }); 39 | 40 | selections.selected(selected); 41 | } 42 | }); 43 | } 44 | 45 | insertListing.render(); 46 | }, 47 | 48 | updateListingFiltersForFirstLoad: function () { 49 | var 50 | insertListing = registry.get(this.insertListing), 51 | filter = {}; 52 | 53 | 54 | filter[insertListing.indexField] = { 55 | 'condition_type': insertListing.externalCondition, 56 | value: [] 57 | }; 58 | 59 | insertListing.set('externalFiltersModifier', filter); 60 | insertListing.needInitialListingUpdate = true; 61 | insertListing.initialUpdateListing(); 62 | 63 | this.listens = []; 64 | this.initLinks(); 65 | 66 | insertListing.firstLoad = false; 67 | } 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/view/grid/columns/multiselect-with-limit.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'mage/translate', 4 | 'Magento_Ui/js/grid/columns/multiselect', 5 | 'uiLayout', 6 | 'Grasch_AdminUi/js/model/messages' 7 | ], function (_, $t, Multiselect, layout, Messages) { 8 | 'use strict'; 9 | 10 | return Multiselect.extend({ 11 | defaults: { 12 | message: $t('You can\'t select more than %limit items.'), 13 | messageType: 'warning' 14 | }, 15 | 16 | /** @inheritdoc */ 17 | initialize: function () { 18 | this._super(); 19 | 20 | if (typeof this.limit == 'undefined') { 21 | throw new Error('limit option is required.'); 22 | } 23 | if (typeof this.limit == 'string') { 24 | this.limit = parseInt(this.limit); 25 | } 26 | 27 | this.messageContainer = new Messages(); 28 | layout([{ 29 | parent: this.ns + '.' + this.ns, 30 | name: 'messages', 31 | component: 'Grasch_AdminUi/js/view/messages', 32 | config: { 33 | messageContainer: this.messageContainer, 34 | sortOrder: 0, 35 | isAlwaysVisible: true, 36 | removeOnClick: false 37 | } 38 | }]); 39 | 40 | this.listens['selected'] = 'validateLimit onSelectedChange'; 41 | this.listens['rows'] = 'validateLimit onSelectedChange'; 42 | this.initLinks(); 43 | 44 | return this; 45 | }, 46 | 47 | /** 48 | * Validate limit 49 | */ 50 | validateLimit: function () { 51 | if (this.selected().length >= this.limit) { 52 | var difference = _.difference(this.getIds(), this.selected()); 53 | this.disabled(difference); 54 | this.showLimitationMessage(); 55 | } else { 56 | this.disabled([]); 57 | this.hideLimitationMessage(); 58 | } 59 | }, 60 | 61 | selectPage: function () { 62 | var difference = _.difference(this.getIds(), this.selected()); 63 | if (difference.length + this.selected().length > this.limit) { 64 | this.showLimitationMessage(); 65 | } else { 66 | this._super(); 67 | } 68 | }, 69 | 70 | /** 71 | * Show limitation message 72 | */ 73 | showLimitationMessage: function () { 74 | var 75 | self = this, 76 | messageConfig; 77 | 78 | messageConfig = _.find(this.messageContainer.messagesConfig(), function (item) { 79 | return item.type == self.messageType; //eslint-disable-line eqeqeq 80 | }); 81 | 82 | this.messageContainer.add( 83 | { 84 | message: this.message, 85 | parameters: { 86 | limit: this.limit 87 | } 88 | }, 89 | messageConfig.messages 90 | ); 91 | }, 92 | 93 | /** 94 | * Hide limitation message 95 | */ 96 | hideLimitationMessage: function () { 97 | this.messageContainer.clear(); 98 | } 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/view/messages.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'ko', 3 | 'jquery', 4 | 'uiComponent', 5 | 'Grasch_AdminUi/js/model/messageList', 6 | 'jquery-ui-modules/effect-blind' 7 | ], function (ko, $, Component, globalMessages) { 8 | 'use strict'; 9 | 10 | return Component.extend({ 11 | defaults: { 12 | template: 'Grasch_AdminUi/messages', 13 | selector: '[data-role=messages]', 14 | isHidden: false, 15 | hideTimeout: 5000, 16 | hideSpeed: 500, 17 | isAlwaysVisible: false, 18 | removeOnClick: true, 19 | listens: { 20 | isHidden: 'onHiddenChange' 21 | } 22 | }, 23 | 24 | /** @inheritdoc */ 25 | initialize: function (config, messageContainer) { 26 | this._super() 27 | .initObservable(); 28 | 29 | this.messageContainer = messageContainer || config.messageContainer || globalMessages; 30 | 31 | return this; 32 | }, 33 | 34 | /** @inheritdoc */ 35 | initObservable: function () { 36 | this._super() 37 | .observe('isHidden'); 38 | 39 | return this; 40 | }, 41 | 42 | /** 43 | * Checks visibility. 44 | * 45 | * @return {Boolean} 46 | */ 47 | isVisible: function () { 48 | if (this.isAlwaysVisible) { 49 | return true; 50 | } 51 | 52 | return this.isHidden(this.messageContainer.hasMessages()); 53 | }, 54 | 55 | /** 56 | * Remove all messages. 57 | */ 58 | removeAll: function () { 59 | if (this.removeOnClick) { 60 | this.messageContainer.clear(); 61 | } 62 | }, 63 | 64 | /** 65 | * @param {Boolean} isHidden 66 | */ 67 | onHiddenChange: function (isHidden) { 68 | // Hide message block if needed 69 | if (isHidden) { 70 | setTimeout(function () { 71 | $(this.selector).hide('blind', {}, this.hideSpeed); 72 | }.bind(this), this.hideTimeout); 73 | } 74 | }, 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/events.js: -------------------------------------------------------------------------------- 1 | define(['uiEvents'], function (uiEvents) { 2 | 'use strict'; 3 | 4 | return { 5 | 6 | /** 7 | * Calls callback when name event is triggered 8 | * 9 | * @param {String} events 10 | * @param {Function} callback 11 | * @param {Function} ns 12 | * @return {Object} 13 | */ 14 | on: function (events, callback, ns) { 15 | uiEvents.on('pagebuilder:' + events, callback, 'pagebuilder:' + ns); 16 | 17 | return this; 18 | }, 19 | 20 | /** 21 | * Removed callback from listening to target event 22 | * 23 | * @param {String} ns 24 | * @return {Object} 25 | */ 26 | off: function (ns) { 27 | uiEvents.off('pagebuilder:' + ns); 28 | 29 | return this; 30 | }, 31 | 32 | /** 33 | * Triggers event and executes all attached callbacks 34 | * 35 | * @param {String} name 36 | * @param {any} args 37 | * @returns {Boolean} 38 | */ 39 | trigger: function (name, args) { 40 | return uiEvents.trigger('pagebuilder:' + name, args); 41 | } 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/form/element/page-builder/wysiwyg.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'mageUtils', 4 | 'uiLayout', 5 | 'Magento_PageBuilder/js/form/element/wysiwyg', 6 | 'ko' 7 | ], function (_, utils, layout, Wysiwyg, ko) { 8 | 'use strict'; 9 | 10 | return Wysiwyg.extend({ 11 | defaults: { 12 | links: { 13 | customValue: '${ $.provider }:${ $.dataScope }', 14 | value: '' 15 | }, 16 | listens: { 17 | value: 'updateValue' 18 | }, 19 | componentInitialized: false 20 | }, 21 | 22 | initialize: function () { 23 | this._super(); 24 | 25 | if (typeof this.content === 'function') { 26 | this.content(Base64.decode(this.content())); 27 | } else { 28 | this.content = Base64.decode(this.content); 29 | } 30 | 31 | if (this.customValue()) { 32 | this.value(Base64.decode(this.customValue())); 33 | } 34 | this.componentInitialized = true; 35 | 36 | return this; 37 | }, 38 | 39 | setInitialValue: function () { 40 | this._super(); 41 | 42 | if (this.customValue()) { 43 | this.initialValue = Base64.decode(this.customValue()); 44 | } 45 | 46 | return this; 47 | }, 48 | 49 | initObservable: function () { 50 | this._super(); 51 | 52 | this.observe('customValue'); 53 | 54 | return this; 55 | }, 56 | 57 | updateValue: function () { 58 | if (!this.componentInitialized) { 59 | return; 60 | } 61 | this.customValue(Base64.encode(this.value())); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/form/element/sync-field.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'mageUtils', 4 | 'uiLayout', 5 | 'Magento_Ui/js/form/element/abstract', 6 | 'ko', 7 | 'jquery' 8 | ], function (_, utils, layout, Abstract, ko, $) { 9 | 'use strict'; 10 | 11 | return Abstract.extend({ 12 | defaults: { 13 | links: { 14 | value: '${ $.provider }:${ $.dataScope }' 15 | }, 16 | listens: { 17 | value: 'updateCustomValue' 18 | }, 19 | valueInitialized: false 20 | }, 21 | 22 | /** 23 | * Initializes observable properties of instance 24 | * 25 | * @returns {Abstract} Chainable. 26 | */ 27 | initObservable: function () { 28 | 29 | this._super(); 30 | this.observe('customValue'); 31 | 32 | return this; 33 | }, 34 | 35 | getInitialValue: function () { 36 | this.valueInitialized = true; 37 | 38 | return this.value(); 39 | }, 40 | 41 | setInitialValue: function () { 42 | this._super(); 43 | 44 | this.updateCustomValue(); 45 | return this; 46 | }, 47 | 48 | updateCustomValue: function () { 49 | if (!this.valueInitialized) { 50 | return; 51 | } 52 | 53 | var 54 | value = this.value(), 55 | customValue; 56 | 57 | if (!value) { 58 | return; 59 | } 60 | 61 | if (typeof value == 'object') { 62 | customValue = JSON.stringify($.extend({}, value)); 63 | } else { 64 | customValue = value; 65 | } 66 | 67 | this.customValue('encodedComponentsData|' + Base64.encode(customValue)); 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/form/element/wysiwyg.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'mageUtils', 4 | 'uiLayout', 5 | 'Magento_Ui/js/form/element/wysiwyg', 6 | 'ko' 7 | ], function (_, utils, layout, Wysiwyg, ko) { 8 | 'use strict'; 9 | 10 | return Wysiwyg.extend({ 11 | defaults: { 12 | links: { 13 | customValue: '${ $.provider }:${ $.dataScope }', 14 | value: '' 15 | }, 16 | listens: { 17 | value: 'updateValue' 18 | }, 19 | componentInitialized: false 20 | }, 21 | 22 | initialize: function () { 23 | this._super(); 24 | 25 | if (typeof this.content === 'function') { 26 | this.content(Base64.decode(this.content())); 27 | } else { 28 | this.content = Base64.decode(this.content); 29 | } 30 | 31 | if (this.customValue()) { 32 | this.value(Base64.decode(this.customValue())); 33 | } 34 | this.componentInitialized = true; 35 | 36 | return this; 37 | }, 38 | 39 | initObservable: function () { 40 | this._super(); 41 | 42 | this.observe('customValue'); 43 | 44 | return this; 45 | }, 46 | 47 | updateValue: function () { 48 | if (!this.componentInitialized) { 49 | return; 50 | } 51 | this.customValue(Base64.encode(this.value())); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/initialization/main.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'jquery', 4 | './scripts', 5 | 'ko', 6 | 'Magento_Ui/js/lib/knockout/bootstrap', 7 | ], function (_, $, processScripts, ko) { 8 | 'use strict'; 9 | 10 | var dataAttr = 'data-mage-init', 11 | nodeSelector = '[' + dataAttr + ']', 12 | bindElemsSelector = '.widget-ui-components'; 13 | 14 | /** 15 | * Initializes components assigned to a specified element via data-* attribute. 16 | * 17 | * @param {HTMLElement} el - Element to initialize components with. 18 | * @param {Object|String} config - Initial components' config. 19 | * @param {String} component - Components' path. 20 | */ 21 | function init(el, config, component) { 22 | require([component], function (fn) { 23 | var $el; 24 | 25 | if (typeof fn === 'object') { 26 | fn = fn[component].bind(fn); 27 | } 28 | 29 | if (_.isFunction(fn)) { 30 | fn = fn.bind(null, config, el); 31 | } else { 32 | $el = $(el); 33 | 34 | if ($el[component]) { 35 | fn = $el[component].bind($el, config); 36 | } 37 | } 38 | // Init module in separate task to prevent blocking main thread. 39 | setTimeout(fn); 40 | }, function (error) { 41 | if ('console' in window && typeof window.console.error === 'function') { 42 | console.error(error); 43 | } 44 | 45 | return true; 46 | }); 47 | } 48 | 49 | /** 50 | * Parses elements 'data-mage-init' attribute as a valid JSON data. 51 | * Note: data-mage-init attribute will be removed. 52 | * 53 | * @param {HTMLElement} el - Element whose attribute should be parsed. 54 | * @returns {Object} 55 | */ 56 | function getData(el) { 57 | var data = el.getAttribute(dataAttr); 58 | 59 | el.removeAttribute(dataAttr); 60 | 61 | return { 62 | el: el, 63 | data: JSON.parse(data) 64 | }; 65 | } 66 | 67 | return { 68 | /** 69 | * Initializes components assigned to HTML elements via [data-mage-init]. 70 | * 71 | * @example Sample 'data-mage-init' declaration. 72 | * data-mage-init='{"path/to/component": {"foo": "bar"}}' 73 | */ 74 | apply: function (context) { 75 | var virtuals = processScripts(!context ? document : context), 76 | nodes = document.querySelectorAll(nodeSelector); 77 | 78 | _.toArray(nodes) 79 | .map(getData) 80 | .concat(virtuals) 81 | .forEach(function (itemContainer) { 82 | var element = itemContainer.el; 83 | 84 | _.each(itemContainer.data, function (obj, key) { 85 | if (obj.mixins) { 86 | require(obj.mixins, function () { //eslint-disable-line max-nested-callbacks 87 | var i, len; 88 | 89 | for (i = 0, len = arguments.length; i < len; i++) { 90 | $.extend( 91 | true, 92 | itemContainer.data[key], 93 | arguments[i](itemContainer.data[key], element) 94 | ); 95 | } 96 | 97 | delete obj.mixins; 98 | init.call(null, element, obj, key); 99 | }); 100 | } else { 101 | init.call(null, element, obj, key); 102 | } 103 | } 104 | ); 105 | 106 | }); 107 | 108 | $(bindElemsSelector).each(function (index, item) { 109 | try { 110 | ko.applyBindings($(item), item); 111 | } catch (e) {} 112 | }); 113 | }, 114 | applyFor: init 115 | }; 116 | }); 117 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/widget/initialization/scripts.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'underscore', 3 | 'jquery' 4 | ], function (_, $) { 5 | 'use strict'; 6 | 7 | var scriptSelector = 'components[type="text/x-magento-init"]', 8 | dataAttr = 'data-mage-init', 9 | virtuals = []; 10 | 11 | /** 12 | * Adds components to the virtual list. 13 | * 14 | * @param {Object} components 15 | */ 16 | function addVirtual(components) { 17 | virtuals.push({ 18 | el: false, 19 | data: components 20 | }); 21 | } 22 | 23 | /** 24 | * Merges provided data with a current data 25 | * of a elements' "data-mage-init" attribute. 26 | * 27 | * @param {Object} components - Object with components and theirs configuration. 28 | * @param {HTMLElement} elem - Element whose data should be modified. 29 | */ 30 | function setData(components, elem) { 31 | var data = elem.getAttribute(dataAttr); 32 | 33 | data = data ? JSON.parse(data) : {}; 34 | _.each(components, function (obj, key) { 35 | if (_.has(obj, 'mixins')) { 36 | data[key] = data[key] || {}; 37 | data[key].mixins = data[key].mixins || []; 38 | data[key].mixins = data[key].mixins.concat(obj.mixins); 39 | delete obj.mixins; 40 | } 41 | }); 42 | 43 | data = $.extend(true, data, components); 44 | data = JSON.stringify(data); 45 | elem.setAttribute(dataAttr, data); 46 | } 47 | 48 | /** 49 | * Search for the elements by privded selector and extends theirs data. 50 | * 51 | * @param {Object} components - Object with components and theirs configuration. 52 | * @param {String} selector - Selector for the elements. 53 | */ 54 | function processElems(components, selector) { 55 | var elems, 56 | iterator; 57 | 58 | if (selector === '*') { 59 | addVirtual(components); 60 | 61 | return; 62 | } 63 | 64 | elems = document.querySelectorAll(selector); 65 | iterator = setData.bind(null, components); 66 | 67 | _.toArray(elems).forEach(iterator); 68 | } 69 | 70 | /** 71 | * Parses content of a provided script node. 72 | * Note: node will be removed from DOM. 73 | * 74 | * @param {HTMLScriptElement} node - Node to be processed. 75 | * @returns {Object} 76 | */ 77 | function getNodeData(node) { 78 | var data = node.textContent; 79 | 80 | node.parentNode.removeChild(node); 81 | 82 | return JSON.parse(data); 83 | } 84 | 85 | /** 86 | * Parses 'script' tags with a custom type attribute and moves it's data 87 | * to a 'data-mage-init' attribute of an element found by provided selector. 88 | * Note: All found script nodes will be removed from DOM. 89 | * 90 | * @returns {Array} An array of components not assigned to the specific element. 91 | * 92 | * @example Sample declaration. 93 | * 100 | * 101 | * @example Providing data without selector. 102 | * { 103 | * "*": { 104 | * "path/to/component": {"bar": "baz"} 105 | * } 106 | * } 107 | */ 108 | return function () { 109 | var nodes = document.querySelectorAll(scriptSelector); 110 | 111 | _.toArray(nodes) 112 | .map(getNodeData) 113 | .forEach(function (item) { 114 | _.each(item, processElems); 115 | }); 116 | 117 | return virtuals.splice(0, virtuals.length); 118 | }; 119 | }); 120 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/wysiwyg/widget.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | 'wysiwygAdapter', 4 | 'Magento_Ui/js/modal/alert', 5 | 'uiRegistry', 6 | 'Grasch_AdminUi/js/widget/events', 7 | 'consoleLogger', 8 | 'jquery/ui', 9 | 'mage/translate', 10 | 'mage/mage', 11 | 'mage/validation', 12 | 'mage/adminhtml/events', 13 | 'prototype', 14 | 'Magento_Ui/js/modal/modal' 15 | ], function (jQuery, wysiwyg, alert, registry, events, consoleLogger) { 16 | var widgetTools = { 17 | 18 | /** 19 | * Sets the widget to be active and is the scope of the slide out if the value is set 20 | */ 21 | activeSelectedNode: null, 22 | editMode: false, 23 | cursorLocation: 0, 24 | 25 | /** 26 | * Set active selected node. 27 | * 28 | * @param {Object} activeSelectedNode 29 | */ 30 | setActiveSelectedNode: function (activeSelectedNode) { 31 | this.activeSelectedNode = activeSelectedNode; 32 | }, 33 | 34 | /** 35 | * Get active selected node. 36 | * 37 | * @returns {null} 38 | */ 39 | getActiveSelectedNode: function () { 40 | return this.activeSelectedNode; 41 | }, 42 | 43 | /** 44 | * 45 | * @param {Boolean} editMode 46 | */ 47 | setEditMode: function (editMode) { 48 | this.editMode = editMode; 49 | }, 50 | 51 | /** 52 | * @param {*} id 53 | * @param {*} html 54 | * @return {String} 55 | */ 56 | getDivHtml: function (id, html) { 57 | 58 | if (!html) { 59 | html = ''; 60 | } 61 | 62 | return '' + html + ''; 63 | }, 64 | 65 | /** 66 | * @param {Object} transport 67 | */ 68 | onAjaxSuccess: function (transport) { 69 | var response; 70 | 71 | if (transport.responseText.isJSON()) { 72 | response = transport.responseText.evalJSON(); 73 | 74 | if (response.error) { 75 | throw response; 76 | } else if (response.ajaxExpired && response.ajaxRedirect) { 77 | setLocation(response.ajaxRedirect); 78 | } 79 | } 80 | }, 81 | 82 | dialogOpened: false, 83 | 84 | /** 85 | * @return {Number} 86 | */ 87 | getMaxZIndex: function () { 88 | var max = 0, 89 | cn = document.body.childNodes, 90 | i, el, zIndex; 91 | 92 | for (i = 0; i < cn.length; i++) { 93 | el = cn[i]; 94 | zIndex = el.nodeType == 1 ? parseInt(el.style.zIndex, 10) || 0 : 0; //eslint-disable-line eqeqeq 95 | 96 | if (zIndex < 10000) { 97 | max = Math.max(max, zIndex); 98 | } 99 | } 100 | 101 | return max + 10; 102 | }, 103 | 104 | /** 105 | * @param {String} widgetUrl 106 | */ 107 | openDialog: function (widgetUrl) { 108 | var oThis = this, 109 | title = 'Insert Widget', 110 | mode = 'new', 111 | dialog; 112 | 113 | if (this.editMode) { 114 | title = 'Edit Widget'; 115 | mode = 'edit'; 116 | } 117 | 118 | if (this.dialogOpened) { 119 | return; 120 | } 121 | 122 | this.dialogWindow = jQuery('').modal({ 123 | 124 | title: jQuery.mage.__(title), 125 | type: 'slide', 126 | buttons: [], 127 | 128 | /** 129 | * Opened. 130 | */ 131 | opened: function () { 132 | dialog = jQuery(this).addClass('loading magento-message'); 133 | 134 | widgetUrl += 'mode/' + mode; 135 | 136 | new Ajax.Updater($(this), widgetUrl, { 137 | evalScripts: true, 138 | 139 | /** 140 | * On complete. 141 | */ 142 | onComplete: function () { 143 | dialog.removeClass('loading'); 144 | } 145 | }); 146 | }, 147 | 148 | /** 149 | * @param {jQuery.Event} e 150 | * @param {Object} modal 151 | */ 152 | closed: function (e, modal) { 153 | jQuery('body').trigger('widget-disappeared'); 154 | modal.modal.remove(); 155 | oThis.dialogOpened = false; 156 | } 157 | }); 158 | 159 | this.dialogOpened = true; 160 | this.dialogWindow.modal('openModal'); 161 | } 162 | }, 163 | WysiwygWidget = {}; 164 | 165 | WysiwygWidget.Widget = Class.create(); 166 | WysiwygWidget.Widget.prototype = { 167 | /** 168 | * @param {HTMLElement} formEl 169 | * @param {HTMLElement} widgetEl 170 | * @param {*} widgetOptionsEl 171 | * @param {*} optionsSourceUrl 172 | * @param {*} widgetTargetId 173 | */ 174 | initialize: function (formEl, widgetEl, widgetOptionsEl, optionsSourceUrl, widgetTargetId) { 175 | this.pageBuilderInstances = []; 176 | 177 | events.on('pagebuilder:register', function (data) { 178 | jQuery('.widget-ui-components').each(function (index, item) { 179 | var 180 | scope = jQuery(item).data('scope'), 181 | ns; 182 | 183 | if (scope) { 184 | var ns = scope.split('.').first(); 185 | } 186 | 187 | if (data.ns === ns) { 188 | this.pageBuilderInstances.push(data.instance); 189 | } 190 | }.bind(this)); 191 | }.bind(this)); 192 | 193 | $(formEl).insert({ 194 | bottom: widgetTools.getDivHtml(widgetOptionsEl) 195 | }); 196 | 197 | this.formEl = formEl; 198 | this.widgetEl = $(widgetEl); 199 | this.widgetOptionsEl = $(widgetOptionsEl); 200 | this.optionsUrl = optionsSourceUrl; 201 | this.optionValues = new Hash({}); 202 | this.widgetTargetId = widgetTargetId; 203 | 204 | if (typeof wysiwyg != 'undefined' && wysiwyg.activeEditor()) { //eslint-disable-line eqeqeq 205 | this.bMark = wysiwyg.activeEditor().selection.getBookmark(); 206 | } 207 | 208 | // disable -- Please Select -- option from being re-selected 209 | this.widgetEl.querySelector('option').setAttribute('disabled', 'disabled'); 210 | 211 | Event.observe(this.widgetEl, 'change', this.loadOptions.bind(this)); 212 | 213 | this.initOptionValues(); 214 | }, 215 | 216 | /** 217 | * @return {String} 218 | */ 219 | getOptionsContainerId: function () { 220 | return this.widgetOptionsEl.id + '_' + this.widgetEl.value.gsub(/\//, '_'); 221 | }, 222 | 223 | /** 224 | * @param {*} containerId 225 | */ 226 | switchOptionsContainer: function (containerId) { 227 | $$('#' + this.widgetOptionsEl.id + ' div[id^=' + this.widgetOptionsEl.id + ']').each(function (e) { 228 | this.disableOptionsContainer(e.id); 229 | }.bind(this)); 230 | 231 | if (containerId != undefined) { //eslint-disable-line eqeqeq 232 | this.enableOptionsContainer(containerId); 233 | } 234 | this._showWidgetDescription(); 235 | }, 236 | 237 | /** 238 | * @param {*} containerId 239 | */ 240 | enableOptionsContainer: function (containerId) { 241 | var container = $(containerId); 242 | 243 | container.select('.widget-option').each(function (e) { 244 | e.removeClassName('skip-submit'); 245 | 246 | if (e.hasClassName('obligatory')) { 247 | e.removeClassName('obligatory'); 248 | e.addClassName('required-entry'); 249 | } 250 | }); 251 | container.removeClassName('no-display'); 252 | }, 253 | 254 | /** 255 | * @param {*} containerId 256 | */ 257 | disableOptionsContainer: function (containerId) { 258 | var container = $(containerId); 259 | 260 | if (container.hasClassName('no-display')) { 261 | return; 262 | } 263 | container.select('.widget-option').each(function (e) { 264 | // Avoid submitting fields of unactive container 265 | if (!e.hasClassName('skip-submit')) { 266 | e.addClassName('skip-submit'); 267 | } 268 | // Form validation workaround for unactive container 269 | if (e.hasClassName('required-entry')) { 270 | e.removeClassName('required-entry'); 271 | e.addClassName('obligatory'); 272 | } 273 | }); 274 | container.addClassName('no-display'); 275 | }, 276 | 277 | /** 278 | * Assign widget options values when existing widget selected in WYSIWYG. 279 | * 280 | * @return {Boolean} 281 | */ 282 | initOptionValues: function () { 283 | var e, widgetCode; 284 | 285 | if (!this.wysiwygExists()) { 286 | return false; 287 | } 288 | 289 | e = this.getWysiwygNode(); 290 | 291 | if (e.localName === 'span') { 292 | e = e.firstElementChild; 293 | } 294 | 295 | if (e != undefined && e.id) { //eslint-disable-line eqeqeq 296 | // attempt to Base64-decode id on selected node; exception is thrown if it is in fact not a widget node 297 | try { 298 | widgetCode = Base64.idDecode(e.id); 299 | } catch (ex) { 300 | return false; 301 | } 302 | 303 | if (widgetCode.indexOf('{{widget') !== -1) { 304 | this.optionValues = new Hash({}); 305 | widgetCode.gsub(/([a-z0-9\_]+)\s*\=\s*[\"]{1}([^\"]+)[\"]{1}/i, function (match) { 306 | 307 | if (match[1] == 'type') { //eslint-disable-line eqeqeq 308 | this.widgetEl.value = match[2]; 309 | } else { 310 | this.optionValues.set(match[1], match[2]); 311 | } 312 | 313 | }.bind(this)); 314 | 315 | this.loadOptions(); 316 | } 317 | } 318 | }, 319 | 320 | /** 321 | * Load options. 322 | */ 323 | loadOptions: function () { 324 | var optionsContainerId, 325 | params, 326 | msg, 327 | msgTmpl, 328 | $wrapper, 329 | typeName = this.optionValues.get('type_name'), 330 | visibleWidgetOptions; 331 | 332 | visibleWidgetOptions = jQuery(this.widgetOptionsEl).children().not('.no-display'); 333 | if (visibleWidgetOptions.length) { 334 | if (visibleWidgetOptions.find('.widget-ui-components').length) { 335 | jQuery('body').trigger('widget-disappeared'); 336 | visibleWidgetOptions.remove(); 337 | } 338 | } 339 | 340 | if (!this.widgetEl.value) { 341 | if (typeName) { 342 | msgTmpl = jQuery.mage.__('The widget %1 is no longer available. Select a different widget.'); 343 | msg = jQuery.mage.__(msgTmpl).replace('%1', typeName); 344 | 345 | jQuery('body').notification('clear').notification('add', { 346 | error: true, 347 | message: msg, 348 | 349 | /** 350 | * @param {String} message 351 | */ 352 | insertMethod: function (message) { 353 | $wrapper = jQuery('').html(message); 354 | 355 | $wrapper.insertAfter('.modal-slide .page-main-actions'); 356 | } 357 | }); 358 | } 359 | this.switchOptionsContainer(); 360 | 361 | return; 362 | } 363 | 364 | optionsContainerId = this.getOptionsContainerId(); 365 | 366 | if ($(optionsContainerId) != undefined) { //eslint-disable-line eqeqeq 367 | this.switchOptionsContainer(optionsContainerId); 368 | 369 | return; 370 | } 371 | 372 | this._showWidgetDescription(); 373 | 374 | params = { 375 | 'widget_type': this.widgetEl.value, 376 | values: this.optionValues 377 | }; 378 | new Ajax.Request(this.optionsUrl, { 379 | parameters: { 380 | widget: Object.toJSON(params) 381 | }, 382 | 383 | /** 384 | * On success. 385 | */ 386 | onSuccess: function (transport) { 387 | try { 388 | widgetTools.onAjaxSuccess(transport); 389 | this.switchOptionsContainer(); 390 | 391 | if ($(optionsContainerId) == undefined) { //eslint-disable-line eqeqeq 392 | this.widgetOptionsEl.insert({ 393 | bottom: widgetTools.getDivHtml(optionsContainerId, transport.responseText) 394 | }); 395 | } else { 396 | this.switchOptionsContainer(optionsContainerId); 397 | } 398 | } catch (e) { 399 | alert({ 400 | content: e.message 401 | }); 402 | } 403 | }.bind(this) 404 | }); 405 | }, 406 | 407 | /** 408 | * @private 409 | */ 410 | _showWidgetDescription: function () { 411 | var noteCnt = this.widgetEl.next().down('small'), 412 | descrCnt = $('widget-description-' + this.widgetEl.selectedIndex), 413 | description; 414 | 415 | if (noteCnt != undefined) { //eslint-disable-line eqeqeq 416 | description = descrCnt != undefined ? descrCnt.innerHTML : ''; //eslint-disable-line eqeqeq 417 | noteCnt.update(description); 418 | } 419 | }, 420 | 421 | /** 422 | * Validate field. 423 | */ 424 | validateField: function () { 425 | jQuery(this.widgetEl).valid(); 426 | jQuery('#insert_button').removeClass('disabled'); 427 | }, 428 | 429 | /** 430 | * Closes the modal 431 | */ 432 | closeModal: function () { 433 | widgetTools.dialogWindow.modal('closeModal'); 434 | }, 435 | 436 | /* eslint-disable max-depth*/ 437 | /** 438 | * Insert widget. 439 | */ 440 | insertWidget: function () { 441 | var validationResult, 442 | $form = jQuery('#' + this.formEl), 443 | formElements, 444 | i, 445 | params, 446 | editor, 447 | activeNode; 448 | 449 | // remove cached validator instance, which caches elements to validate 450 | jQuery.data($form[0], 'validator', null); 451 | 452 | $form.validate({ 453 | /** 454 | * Ignores elements with .skip-submit, .no-display ancestor elements 455 | */ 456 | ignore: function () { 457 | return jQuery(this).closest('.skip-submit, .no-display').length; 458 | }, 459 | errorClass: 'mage-error' 460 | }); 461 | 462 | validationResult = $form.valid(); 463 | 464 | if (validationResult) { 465 | jQuery('.widget-ui-components').each(function (index, item) { 466 | var path = jQuery(item).data('scope'); 467 | if (registry.get(path)) { 468 | registry.get(path).source.set('params.invalid', false); 469 | registry.get(path).source.trigger('data.validate'); 470 | if (registry.get(path).source.get('params.invalid')) { 471 | validationResult = false; 472 | return false; 473 | } 474 | } 475 | }); 476 | } 477 | 478 | var submit = function () { 479 | formElements = []; 480 | i = 0; 481 | Form.getElements($(this.formEl)).each(function (e) { 482 | 483 | if (jQuery(e).closest('.skip-submit, .no-display').length === 0) { 484 | formElements[i] = e; 485 | i++; 486 | } 487 | }); 488 | 489 | // Add as_is flag to parameters if wysiwyg editor doesn't exist 490 | params = Form.serializeElements(formElements); 491 | 492 | if (!this.wysiwygExists()) { 493 | params += '&as_is=1'; 494 | } 495 | 496 | new Ajax.Request($(this.formEl).action, { 497 | parameters: params, 498 | onComplete: function (transport) { 499 | try { 500 | editor = wysiwyg.get(this.widgetTargetId); 501 | 502 | widgetTools.onAjaxSuccess(transport); 503 | widgetTools.dialogWindow.modal('closeModal'); 504 | 505 | if (editor) { 506 | editor.focus(); 507 | activeNode = widgetTools.getActiveSelectedNode(); 508 | 509 | if (activeNode) { 510 | editor.selection.select(activeNode); 511 | editor.selection.setContent(transport.responseText); 512 | editor.fire('Change'); 513 | } else if (this.bMark) { 514 | editor.selection.moveToBookmark(this.bMark); 515 | } 516 | } 517 | 518 | if (!activeNode) { 519 | this.updateContent(transport.responseText); 520 | } 521 | } catch (e) { 522 | alert({ 523 | content: e.message 524 | }); 525 | } 526 | }.bind(this) 527 | }); 528 | }.bind(this); 529 | 530 | if (validationResult) { 531 | if (window.isPageBuilderEnabled) { 532 | var timeout, locks; 533 | 534 | if (_.isEmpty(this.pageBuilderInstances)) { 535 | submit(); 536 | } else { 537 | timeout = setTimeout(function () { 538 | consoleLogger.error('Page Builder was rendering for 5 seconds without releasing locks.'); 539 | }, 5000); 540 | 541 | jQuery('body').trigger('processStart'); 542 | 543 | // Wait for all rendering locks within Page Builder stages to resolve 544 | jQuery.when.apply( 545 | null, 546 | this.pageBuilderInstances.map(function (instance) { 547 | locks = instance.stage.renderingLocks; 548 | 549 | return locks[locks.length - 1]; 550 | }) 551 | ).then(function () { 552 | jQuery('body').trigger('processStop'); 553 | clearTimeout(timeout); 554 | submit(); 555 | }); 556 | } 557 | } else { 558 | submit(); 559 | } 560 | } 561 | }, 562 | 563 | /** 564 | * @param {Object} content 565 | */ 566 | updateContent: function (content) { 567 | var textarea; 568 | 569 | if (this.wysiwygExists()) { 570 | wysiwyg.insertContent(content, false); 571 | } else { 572 | textarea = document.getElementById(this.widgetTargetId); 573 | updateElementAtCursor(textarea, content); 574 | varienGlobalEvents.fireEvent('tinymceChange'); 575 | jQuery(textarea).change(); 576 | } 577 | }, 578 | 579 | /** 580 | * @return {Boolean} 581 | */ 582 | wysiwygExists: function () { 583 | return typeof wysiwyg != 'undefined' && wysiwyg.get(this.widgetTargetId); 584 | }, 585 | 586 | /** 587 | * @return {null|wysiwyg.Editor|*} 588 | */ 589 | getWysiwyg: function () { 590 | return wysiwyg.get(this.widgetTargetId); 591 | }, 592 | 593 | /** 594 | * @return {*|Element} 595 | */ 596 | getWysiwygNode: function () { 597 | return widgetTools.getActiveSelectedNode() || wysiwyg.activeEditor().selection.getNode(); 598 | } 599 | }; 600 | 601 | WysiwygWidget.chooser = Class.create(); 602 | WysiwygWidget.chooser.prototype = { 603 | 604 | // HTML element A, on which click event fired when choose a selection 605 | chooserId: null, 606 | 607 | // Source URL for Ajax requests 608 | chooserUrl: null, 609 | 610 | // Chooser config 611 | config: null, 612 | 613 | // Chooser dialog window 614 | dialogWindow: null, 615 | 616 | // Chooser content for dialog window 617 | dialogContent: null, 618 | 619 | overlayShowEffectOptions: null, 620 | overlayHideEffectOptions: null, 621 | 622 | /** 623 | * @param {*} chooserId 624 | * @param {*} chooserUrl 625 | * @param {*} config 626 | */ 627 | initialize: function (chooserId, chooserUrl, config) { 628 | this.chooserId = chooserId; 629 | this.chooserUrl = chooserUrl; 630 | this.config = config; 631 | }, 632 | 633 | /** 634 | * @return {String} 635 | */ 636 | getResponseContainerId: function () { 637 | return 'responseCnt' + this.chooserId; 638 | }, 639 | 640 | /** 641 | * @return {jQuery|*|HTMLElement} 642 | */ 643 | getChooserControl: function () { 644 | return $(this.chooserId + 'control'); 645 | }, 646 | 647 | /** 648 | * @return {jQuery|*|HTMLElement} 649 | */ 650 | getElement: function () { 651 | return $(this.chooserId + 'value'); 652 | }, 653 | 654 | /** 655 | * @return {jQuery|*|HTMLElement} 656 | */ 657 | getElementLabel: function () { 658 | return $(this.chooserId + 'label'); 659 | }, 660 | 661 | /** 662 | * Open. 663 | */ 664 | open: function () { 665 | $(this.getResponseContainerId()).show(); 666 | }, 667 | 668 | /** 669 | * Close. 670 | */ 671 | close: function () { 672 | $(this.getResponseContainerId()).hide(); 673 | this.closeDialogWindow(); 674 | }, 675 | 676 | /** 677 | * Choose. 678 | */ 679 | choose: function () { 680 | // Open dialog window with previously loaded dialog content 681 | var responseContainerId; 682 | 683 | if (this.dialogContent) { 684 | this.openDialogWindow(this.dialogContent); 685 | 686 | return; 687 | } 688 | // Show or hide chooser content if it was already loaded 689 | responseContainerId = this.getResponseContainerId(); 690 | 691 | // Otherwise load content from server 692 | new Ajax.Request(this.chooserUrl, { 693 | parameters: { 694 | 'element_value': this.getElementValue(), 695 | 'element_label': this.getElementLabelText() 696 | }, 697 | 698 | /** 699 | * On success. 700 | */ 701 | onSuccess: function (transport) { 702 | try { 703 | widgetTools.onAjaxSuccess(transport); 704 | this.dialogContent = widgetTools.getDivHtml(responseContainerId, transport.responseText); 705 | this.openDialogWindow(this.dialogContent); 706 | } catch (e) { 707 | alert({ 708 | content: e.message 709 | }); 710 | } 711 | }.bind(this) 712 | }); 713 | }, 714 | 715 | /** 716 | * Open dialog winodw. 717 | * 718 | * @param {*} content 719 | */ 720 | openDialogWindow: function (content) { 721 | this.dialogWindow = jQuery('').modal({ 722 | title: this.config.buttons.open, 723 | type: 'slide', 724 | buttons: [], 725 | 726 | /** 727 | * Opened. 728 | */ 729 | opened: function () { 730 | jQuery(this).addClass('magento-message'); 731 | }, 732 | 733 | /** 734 | * @param {jQuery.Event} e 735 | * @param {Object} modal 736 | */ 737 | closed: function (e, modal) { 738 | modal.modal.remove(); 739 | this.dialogWindow = null; 740 | } 741 | }); 742 | 743 | this.dialogWindow.modal('openModal').append(content); 744 | }, 745 | 746 | /** 747 | * Close dialog window. 748 | */ 749 | closeDialogWindow: function () { 750 | this.dialogWindow.modal('closeModal').remove(); 751 | }, 752 | 753 | /** 754 | * @return {*|Number} 755 | */ 756 | getElementValue: function () { 757 | return this.getElement().value; 758 | }, 759 | 760 | /** 761 | * @return {String} 762 | */ 763 | getElementLabelText: function () { 764 | return this.getElementLabel().innerHTML; 765 | }, 766 | 767 | /** 768 | * @param {*} value 769 | */ 770 | setElementValue: function (value) { 771 | this.getElement().value = value; 772 | }, 773 | 774 | /** 775 | * @param {*} value 776 | */ 777 | setElementLabel: function (value) { 778 | this.getElementLabel().innerHTML = value; 779 | } 780 | }; 781 | 782 | window.WysiwygWidget = WysiwygWidget; 783 | window.widgetTools = widgetTools; 784 | }); 785 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/entities-selector/complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/widget/form/element/hidden.html: -------------------------------------------------------------------------------- 1 | 11 | --------------------------------------------------------------------------------