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

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