vendor/pimcore/pimcore/models/Document/Editable/Areablock.php line 32

  1. <?php
  2. /**
  3. * This source file is available under the terms of the
  4. * Pimcore Open Core License (POCL)
  5. * Full copyright and license information is available in
  6. * LICENSE.md which is distributed with this source code.
  7. *
  8. * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
  9. * @license Pimcore Open Core License (POCL)
  10. */
  11. namespace Pimcore\Model\Document\Editable;
  12. use Exception;
  13. use Generator;
  14. use Pimcore;
  15. use Pimcore\Document\Editable\Block\BlockName;
  16. use Pimcore\Document\Editable\EditableHandler;
  17. use Pimcore\Extension\Document\Areabrick\AreabrickManagerInterface;
  18. use Pimcore\Extension\Document\Areabrick\EditableDialogBoxInterface;
  19. use Pimcore\Model;
  20. use Pimcore\Model\Document;
  21. use Pimcore\Templating\Renderer\EditableRenderer;
  22. use Pimcore\Tool\HtmlUtils;
  23. /**
  24. * @method \Pimcore\Model\Document\Editable\Dao getDao()
  25. */
  26. class Areablock extends Model\Document\Editable implements BlockInterface
  27. {
  28. /**
  29. * Contains an array of indices, which represent the order of the elements in the block
  30. *
  31. * @internal
  32. *
  33. */
  34. protected array $indices = [];
  35. /**
  36. * Current step of the block while iteration
  37. *
  38. * @internal
  39. *
  40. */
  41. protected int $current = 0;
  42. /**
  43. * @internal
  44. *
  45. */
  46. protected ?array $currentIndex = null;
  47. /**
  48. * @internal
  49. *
  50. */
  51. protected ?bool $blockStarted = false;
  52. /**
  53. * @internal
  54. *
  55. */
  56. protected array $brickTypeUsageCounter = [];
  57. public function getType(): string
  58. {
  59. return 'areablock';
  60. }
  61. public function getData(): mixed
  62. {
  63. return $this->indices;
  64. }
  65. public function admin(): void
  66. {
  67. $this->frontend();
  68. }
  69. public function frontend(): void
  70. {
  71. reset($this->indices);
  72. while ($this->loop());
  73. }
  74. /**
  75. * @internal
  76. *
  77. * @return ($return is true ? string : void)
  78. */
  79. public function renderIndex(int $index, bool $return = false)
  80. {
  81. $this->start($return);
  82. $this->currentIndex = $this->indices[$index];
  83. $this->current = $index;
  84. $this->blockConstruct();
  85. $templateParams = $this->blockStart();
  86. $content = $this->content(null, $templateParams, $return);
  87. if (!$return) {
  88. echo $content;
  89. }
  90. $this->blockDestruct();
  91. $this->blockEnd();
  92. $this->end($return);
  93. if ($return) {
  94. return $content;
  95. }
  96. }
  97. public function getIterator(): Generator
  98. {
  99. while ($this->loop()) {
  100. yield $this->getCurrentIndex();
  101. }
  102. }
  103. /**
  104. * @internal
  105. *
  106. */
  107. public function loop(): bool
  108. {
  109. $disabled = false;
  110. $config = $this->getConfig();
  111. $manual = (($config['manual'] ?? false) == true);
  112. if ($this->current > 0) {
  113. if (!$manual && $this->blockStarted) {
  114. $this->blockDestruct();
  115. $this->blockEnd();
  116. }
  117. } else {
  118. if (!$manual) {
  119. $this->start();
  120. }
  121. }
  122. if ($this->current < count($this->indices) && $this->current < $config['limit']) {
  123. $index = current($this->indices);
  124. next($this->indices);
  125. $this->currentIndex = $index;
  126. if (!empty($config['allowed']) && !in_array($index['type'], $config['allowed'])) {
  127. $disabled = true;
  128. }
  129. $brickTypeLimit = $config['limits'][$this->currentIndex['type']] ?? 100000;
  130. $brickTypeUsageCounter = $this->brickTypeUsageCounter[$this->currentIndex['type']] ?? 0;
  131. if ($brickTypeUsageCounter >= $brickTypeLimit) {
  132. $disabled = true;
  133. }
  134. $this->blockStarted = false;
  135. $info = $this->buildInfoObject();
  136. if (!$manual && !$disabled) {
  137. $this->blockConstruct();
  138. $templateParams = $this->blockStart($info);
  139. $this->content($info, $templateParams);
  140. } elseif (!$manual) {
  141. $this->current++;
  142. }
  143. return true;
  144. } else {
  145. if (!$manual) {
  146. $this->end();
  147. }
  148. return false;
  149. }
  150. }
  151. /**
  152. * @internal
  153. *
  154. */
  155. public function buildInfoObject(): Area\Info
  156. {
  157. $config = $this->getConfig();
  158. // create info object and assign it to the view
  159. $info = new Area\Info();
  160. $info->setId($this->currentIndex ? $this->currentIndex['type'] : null);
  161. $info->setEditable($this);
  162. $info->setIndex($this->current);
  163. $params = [];
  164. if (is_array($config['params'][$this->currentIndex['type']] ?? null)) {
  165. $params = $config['params'][$this->currentIndex['type']];
  166. }
  167. if (is_array($config['globalParams'] ?? null)) {
  168. $params = array_merge($config['globalParams'], $params);
  169. }
  170. $info->setParams($params);
  171. return $info;
  172. }
  173. /**
  174. * @param null|Document\Editable\Area\Info $info
  175. *
  176. * @return string|void
  177. */
  178. public function content(?Area\Info $info = null, array $templateParams = [], bool $return = false)
  179. {
  180. if (!$info) {
  181. $info = $this->buildInfoObject();
  182. }
  183. $content = '';
  184. if ($this->editmode || !isset($this->currentIndex['hidden']) || !$this->currentIndex['hidden']) {
  185. $templateParams['isAreaBlock'] = true;
  186. $content = $this->getEditableHandler()->renderAreaFrontend($info, $templateParams);
  187. if (!$return) {
  188. echo $content;
  189. }
  190. $this->brickTypeUsageCounter += [$this->currentIndex['type'] => 0];
  191. $this->brickTypeUsageCounter[$this->currentIndex['type']]++;
  192. }
  193. $this->current++;
  194. if ($return) {
  195. return $content;
  196. }
  197. }
  198. /**
  199. * @internal
  200. *
  201. */
  202. protected function getEditableHandler(): EditableHandler
  203. {
  204. // TODO inject area handler via DI when editables are built through container
  205. return Pimcore::getContainer()->get(EditableHandler::class);
  206. }
  207. public function setDataFromResource(mixed $data): static
  208. {
  209. $unserializedData = $this->getUnserializedData($data) ?? [];
  210. $this->indices = $unserializedData;
  211. return $this;
  212. }
  213. public function setDataFromEditmode(mixed $data): static
  214. {
  215. $this->indices = $data;
  216. return $this;
  217. }
  218. public function blockConstruct(): void
  219. {
  220. // set the current block suffix for the child elements (0, 1, 3, ...)
  221. // this will be removed in blockDestruct
  222. $this->getBlockState()->pushIndex($this->indices[$this->current]['key']);
  223. }
  224. public function blockDestruct(): void
  225. {
  226. $this->getBlockState()->popIndex();
  227. }
  228. private function getToolBarDefaultConfig(): array
  229. {
  230. return [
  231. 'areablock_toolbar' => [
  232. 'width' => 172,
  233. 'buttonWidth' => 168,
  234. 'buttonMaxCharacters' => 20,
  235. ],
  236. ];
  237. }
  238. public function getEditmodeDefinition(): array
  239. {
  240. $config = array_merge($this->getToolBarDefaultConfig(), $this->getConfig());
  241. $options = parent::getEditmodeDefinition();
  242. $options = array_merge($options, [
  243. 'config' => $config,
  244. ]);
  245. return $options;
  246. }
  247. protected function getEditmodeElementAttributes(): array
  248. {
  249. $attributes = parent::getEditmodeElementAttributes();
  250. $attributes = array_merge($attributes, [
  251. 'name' => $this->getName(),
  252. 'type' => $this->getType(),
  253. ]);
  254. return $attributes;
  255. }
  256. public function start(bool $return = false)
  257. {
  258. if (($this->config['manual'] ?? false) === true) {
  259. // in manual mode $this->render() is not called for the areablock, so we need to add
  260. // the editable to the collector manually here
  261. if ($editableDefCollector = $this->getEditableDefinitionCollector()) {
  262. $editableDefCollector->add($this);
  263. }
  264. }
  265. reset($this->indices);
  266. // set name suffix for the whole block element, this will be added to all child elements of the block
  267. $this->getBlockState()->pushBlock(BlockName::createFromEditable($this));
  268. $attributes = $this->getEditmodeElementAttributes();
  269. $attributeString = HtmlUtils::assembleAttributeString($attributes);
  270. $html = '<div ' . $attributeString . '>';
  271. if ($return) {
  272. return $html;
  273. }
  274. $this->outputEditmode($html);
  275. return $this;
  276. }
  277. public function end(bool $return = false)
  278. {
  279. $this->current = 0;
  280. // remove the current block which was set by $this->start()
  281. $this->getBlockState()->popBlock();
  282. $html = '</div>';
  283. if ($return) {
  284. return $html;
  285. }
  286. $this->outputEditmode($html);
  287. }
  288. public function blockStart(?Area\Info $info = null): array
  289. {
  290. $this->blockStarted = true;
  291. $attributes = [
  292. 'data-name' => $this->getName(),
  293. 'data-real-name' => $this->getRealName(),
  294. ];
  295. $hidden = 'false';
  296. if (isset($this->indices[$this->current]['hidden']) && $this->indices[$this->current]['hidden']) {
  297. $hidden = 'true';
  298. }
  299. $outerAttributes = [
  300. 'key' => $this->indices[$this->current]['key'],
  301. // `data-key` mirrors `key` for editmode consumers: `key` is a reserved
  302. // React prop / non-standard attribute and can be stripped downstream,
  303. // while `data-*` survives. The editmode UI must read `data-key` first.
  304. 'data-key' => $this->indices[$this->current]['key'],
  305. 'type' => $this->indices[$this->current]['type'],
  306. 'data-hidden' => $hidden,
  307. ];
  308. $areabrickManager = Pimcore::getContainer()->get(AreabrickManagerInterface::class);
  309. $dialogConfig = null;
  310. $brick = $areabrickManager->getBrick($this->indices[$this->current]['type']);
  311. if ($this->getEditmode() && $brick instanceof EditableDialogBoxInterface) {
  312. $dialogConfig = $brick->getEditableDialogBoxConfiguration($this, $info);
  313. if ($dialogConfig->getItems()) {
  314. $dialogConfig->setId('dialogBox-' . $this->getName() . '-' . $this->indices[$this->current]['key']);
  315. } else {
  316. $dialogConfig = null;
  317. }
  318. }
  319. $attr = HtmlUtils::assembleAttributeString($attributes);
  320. $oAttr = HtmlUtils::assembleAttributeString($outerAttributes);
  321. $dialogAttributes = '';
  322. if ($dialogConfig) {
  323. $dialogAttributes = HtmlUtils::assembleAttributeString([
  324. 'data-dialog-id' => $dialogConfig->getId(),
  325. ]);
  326. }
  327. $dialogHtml = '';
  328. if ($dialogConfig) {
  329. $editableRenderer = Pimcore::getContainer()->get(EditableRenderer::class);
  330. $items = $this->renderDialogBoxEditables($dialogConfig->getItems(), $editableRenderer, $dialogConfig->getId(), $dialogHtml);
  331. $dialogConfig->setItems($items);
  332. }
  333. return [
  334. 'editmodeOuterAttributes' => $oAttr,
  335. 'editmodeGenericAttributes' => $attr,
  336. 'editableDialog' => $dialogConfig,
  337. 'editableDialogAttributes' => $dialogAttributes,
  338. 'dialogHtml' => $dialogHtml,
  339. ];
  340. }
  341. /**
  342. * This method needs to be `protected` as it is used in other bundles such as pimcore/headless-documents
  343. *
  344. *
  345. * @throws Exception
  346. *
  347. * @internal
  348. */
  349. protected function renderDialogBoxEditables(array|Document\Editable $config, EditableRenderer $editableRenderer, string $dialogId, string &$html): array
  350. {
  351. if ($config instanceof BlockInterface || $config instanceof Area) {
  352. // Unsupported element was passed (e.g., Block, Areablock, ...)
  353. // or an Areas was passed, which is not supported to avoid too long editable names
  354. throw new Exception(sprintf('Using editables of type "%s" for the editable dialog "%s" is not supported.', get_debug_type($config), $dialogId));
  355. } elseif ($config instanceof Document\Editable) {
  356. // Map editable to array config
  357. $config = [
  358. 'type' => $config->getType(),
  359. 'name' => $config->getName(),
  360. 'label' => $config->getLabel(),
  361. 'config' => $config->getConfig(),
  362. 'description' => $config->getDialogDescription(),
  363. ];
  364. }
  365. if (isset($config['items']) && is_array($config['items'])) {
  366. // layout component
  367. foreach ($config['items'] as $index => $child) {
  368. $config['items'][$index] = $this->renderDialogBoxEditables($child, $editableRenderer, $dialogId, $html);
  369. }
  370. } elseif (isset($config['name']) && isset($config['type'])) {
  371. $editable = $editableRenderer->getEditable($this->getDocument(), $config['type'], $config['name'], $config['config'] ?? []);
  372. if (!$editable instanceof Document\Editable) {
  373. throw new Exception(sprintf('Invalid editable type "%s" configured for Dialog Box', $config['type']));
  374. }
  375. $editable->setInDialogBox($dialogId);
  376. $editable->addConfig('dialogBoxConfig', $config);
  377. $html .= $editable->render();
  378. } else {
  379. foreach ($config as $index => $item) {
  380. $config['items'][$index] = $this->renderDialogBoxEditables($item, $editableRenderer, $dialogId, $html);
  381. }
  382. }
  383. return $config;
  384. }
  385. public function blockEnd(): void
  386. {
  387. $this->blockStarted = false;
  388. }
  389. public function setConfig(array $config): static
  390. {
  391. // we need to set this here otherwise custom areaDir's won't work
  392. $this->config = $config;
  393. if (!isset($config['allowed']) || !is_array($config['allowed'])) {
  394. $config['allowed'] = [];
  395. }
  396. $availableAreas = $this->getEditableHandler()->getAvailableAreablockAreas($this, $config);
  397. $availableAreas = $this->sortAvailableAreas($availableAreas, $config);
  398. $config['types'] = $availableAreas;
  399. if (isset($config['group']) && is_array($config['group'])) {
  400. $groupingareas = [];
  401. foreach ($availableAreas as $area) {
  402. $groupingareas[$area['type']] = $area['type'];
  403. }
  404. $groups = [];
  405. foreach ($config['group'] as $name => $areas) {
  406. $n = $name;
  407. $groups[$n] = $areas;
  408. foreach ($areas as $area) {
  409. unset($groupingareas[$area]);
  410. }
  411. }
  412. if (count($groupingareas) > 0) {
  413. $uncatAreas = [];
  414. foreach ($groupingareas as $area) {
  415. $uncatAreas[] = $area;
  416. }
  417. $n = 'Uncategorized';
  418. $groups[$n] = $uncatAreas;
  419. }
  420. $config['group'] = $groups;
  421. }
  422. if (empty($config['limit'])) {
  423. $config['limit'] = 1000000;
  424. }
  425. $config['blockStateStack'] = json_encode($this->getBlockStateStack());
  426. $this->config = $config;
  427. if (($this->config['manual'] ?? false) === true) {
  428. $this->config['reload'] = true;
  429. }
  430. return $this;
  431. }
  432. /**
  433. * Sorts areas by index (sorting option) first, then by name
  434. */
  435. private function sortAvailableAreas(array $areas, array $config): array
  436. {
  437. if (isset($config['sorting']) && is_array($config['sorting']) && count($config['sorting'])) {
  438. $sorting = $config['sorting'];
  439. } else {
  440. if (isset($config['allowed']) && is_array($config['allowed']) && count($config['allowed'])) {
  441. $sorting = $config['allowed'];
  442. } else {
  443. $sorting = [];
  444. }
  445. }
  446. $result = [
  447. 'name' => [],
  448. 'index' => [],
  449. ];
  450. foreach ($areas as $area) {
  451. $sortIndex = false;
  452. if (!empty($sorting)) {
  453. $sortIndex = array_search($area['type'], $sorting);
  454. }
  455. $sortKey = 'name'; // allowed and sorting is not set || areaName is not in allowed
  456. if (false !== $sortIndex) {
  457. $sortKey = 'index';
  458. $area['sortIndex'] = $sortIndex;
  459. }
  460. $result[$sortKey][] = $area;
  461. }
  462. // sort with translated names
  463. if (count($result['name'])) {
  464. usort($result['name'], function ($a, $b) {
  465. if ($a['name'] == $b['name']) {
  466. return 0;
  467. }
  468. return ($a['name'] < $b['name']) ? -1 : 1;
  469. });
  470. }
  471. // sort by allowed brick config order
  472. if (count($result['index'])) {
  473. usort($result['index'], function ($a, $b) {
  474. return $a['sortIndex'] - $b['sortIndex'];
  475. });
  476. }
  477. $result = array_merge($result['index'], $result['name']);
  478. return $result;
  479. }
  480. public function getCount(): int
  481. {
  482. return count($this->indices);
  483. }
  484. public function getCurrent(): int
  485. {
  486. return $this->current - 1;
  487. }
  488. public function getCurrentIndex(): int
  489. {
  490. return $this->indices[$this->getCurrent()]['key'] ?? 0;
  491. }
  492. public function getIndices(): array
  493. {
  494. return $this->indices;
  495. }
  496. /**
  497. * If object was serialized, set the counter back to 0
  498. */
  499. public function __wakeup(): void
  500. {
  501. $this->current = 0;
  502. reset($this->indices);
  503. }
  504. public function isEmpty(): bool
  505. {
  506. return !(bool) count($this->indices);
  507. }
  508. /**
  509. *
  510. * @return Areablock\Item[]
  511. */
  512. public function getElement(string $name): array
  513. {
  514. $document = $this->getDocument();
  515. $parentBlockNames = $this->getParentBlockNames();
  516. $parentBlockNames[] = $this->getName();
  517. $list = [];
  518. foreach ($this->getData() as $index => $item) {
  519. if ($item['type'] === $name) {
  520. $list[$index] = new Areablock\Item($document, $parentBlockNames, (int)$item['key']);
  521. }
  522. }
  523. return $list;
  524. }
  525. }