node.class.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. <?php
  2. /**
  3. * The base node class file of zin of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2023 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
  6. * @author Hao Sun <sunhao@easycorp.ltd>
  7. * @package zin
  8. * @version $Id
  9. * @link https://www.zentao.net
  10. */
  11. namespace zin;
  12. require_once __DIR__ . DS . 'helper.func.php';
  13. require_once __DIR__ . DS . 'selector.func.php';
  14. require_once __DIR__ . DS . 'props.class.php';
  15. /**
  16. * The base node class.
  17. */
  18. class node implements \JsonSerializable
  19. {
  20. /**
  21. * Define properties
  22. *
  23. * @access public
  24. * @var array
  25. */
  26. protected static $defineProps = array();
  27. /**
  28. * Default properties
  29. *
  30. * @access public
  31. * @var array
  32. */
  33. protected static $defaultProps = array();
  34. /**
  35. * @var mixed[]
  36. */
  37. protected static $defineBlocks = array();
  38. /**
  39. * @var string
  40. */
  41. public $gid;
  42. /**
  43. * @var \zin\node|null
  44. */
  45. public $parent;
  46. /**
  47. * @var \zin\node|null
  48. */
  49. public $rootNode;
  50. /**
  51. * @var \zin\props
  52. */
  53. public $props;
  54. /**
  55. * @var mixed[]
  56. */
  57. public $blocks = array();
  58. /**
  59. * @var bool
  60. */
  61. public $removed = false;
  62. /**
  63. * @var mixed[]|null
  64. */
  65. public $replacedWith;
  66. /**
  67. * @var \zin\stdClass|null
  68. */
  69. public $buildData;
  70. /**
  71. * @var mixed[]
  72. */
  73. public $eventBindings = array();
  74. /**
  75. * @var bool|null
  76. */
  77. public $notRenderInGlobal;
  78. /**
  79. * @param mixed ...$args
  80. */
  81. public function __construct(...$args)
  82. {
  83. $this->gid = static::nextGid();
  84. $this->props = new props();
  85. disableGlobalRender();
  86. $this->setDefaultProps(static::getDefaultProps());
  87. $this->add($args);
  88. $this->created();
  89. enableGlobalRender();
  90. renderInGlobal($this);
  91. }
  92. public function __debugInfo()
  93. {
  94. return (array)$this->toJSON();
  95. }
  96. public function __toString()
  97. {
  98. return $this->render();
  99. }
  100. public function fullType()
  101. {
  102. return get_called_class();
  103. }
  104. public function type()
  105. {
  106. $type = $this->fullType();
  107. if(str_contains($type, '\\'))
  108. {
  109. $type = substr($type, strrpos($type, '\\') + 1);
  110. }
  111. return $type;
  112. }
  113. public function id()
  114. {
  115. return strval($this->props->get('id'));
  116. }
  117. public function displayID()
  118. {
  119. $displayID = $this->fullType() . '~' . $this->gid;
  120. $id = $this->id();
  121. if(!empty($id)) $displayID .= "#$id";
  122. return $displayID;
  123. }
  124. /**
  125. * Check if the element is match any of the selectors
  126. * @param string|array|object $selectors
  127. */
  128. public function is($selectors)
  129. {
  130. $list = parseSelectors($selectors);
  131. foreach($list as $selector)
  132. {
  133. if($this->isMatch($selector)) return true;
  134. }
  135. return false;
  136. }
  137. /**
  138. * @param object $selector
  139. */
  140. public function isMatch($selector)
  141. {
  142. if(!empty($selector->id) && $this->id() !== $selector->id) return false;
  143. if(!empty($selector->tag) && $this->type() !== $selector->tag) return false;
  144. if(!empty($selector->class) && !$this->props->class->has($selector->class)) return false;
  145. if(!empty($selector->parents) && !$this->hasParents(...$selector->parents)) return false;
  146. return true;
  147. }
  148. /**
  149. * @param object|string ...$parentSelectors
  150. */
  151. public function hasParents(...$parentSelectors)
  152. {
  153. $parent = $this->parent;
  154. if(!$parent) return false;
  155. foreach($parentSelectors as $selector)
  156. {
  157. $parent = $parent->closest($selector);
  158. if(!$parent) return false;
  159. }
  160. return true;
  161. }
  162. /**
  163. * @param string $event
  164. */
  165. public function off($event)
  166. {
  167. unset($this->eventBindings[$event]);
  168. $this->props->remove("@$event");
  169. }
  170. /**
  171. * @param string|mixed[]|object $selectors
  172. */
  173. public function closest($selectors)
  174. {
  175. $list = parseSelectors($selectors, true);
  176. $node = $this;
  177. while($node)
  178. {
  179. if($node->is($list)) return $node;
  180. $node = $node->parent;
  181. }
  182. return null;
  183. }
  184. /**
  185. * @param string|mixed[]|object $selectors
  186. * @param bool $first
  187. * @param bool $reverse
  188. * @param bool $prebuild
  189. */
  190. public function find($selectors, $first = false, $reverse = false, $prebuild = true)
  191. {
  192. if($prebuild) $this->prebuild();
  193. return findInNode(parseSelectors($selectors, true), $this, $first, $reverse);
  194. }
  195. /**
  196. * @param string|mixed[]|object $selectors
  197. */
  198. public function findFirst($selectors)
  199. {
  200. $results = $this->find($selectors, true);
  201. return empty($results) ? null : reset($results);
  202. }
  203. /**
  204. * @param string|mixed[]|object $selectors
  205. */
  206. public function findLast($selectors)
  207. {
  208. $results = $this->find($selectors, true, true);
  209. return empty($results) ? null : end($results);
  210. }
  211. /**
  212. * @param mixed[]|string $name
  213. * @param mixed $defaultValue
  214. * @return mixed
  215. */
  216. public function prop($name, $defaultValue = null)
  217. {
  218. if(is_array($name))
  219. {
  220. $values = array();
  221. foreach($name as $index => $propName)
  222. {
  223. $values[] = $this->onGetProp($propName, is_array($defaultValue) ? (isset($defaultValue[$propName]) ? $defaultValue[$propName] : $defaultValue[$index]) : $defaultValue);
  224. }
  225. return $values;
  226. }
  227. return $this->onGetProp($name, $defaultValue);
  228. }
  229. /**
  230. * Set property, an array can be passed to set multiple properties
  231. *
  232. * @access public
  233. * @param props|array|string $prop - Property name or properties list
  234. * @param mixed $value - Property value
  235. */
  236. public function setProp($prop, $value = null)
  237. {
  238. if($prop instanceof props) $prop = $prop->toJSON();
  239. if(is_array($prop))
  240. {
  241. foreach($prop as $name => $value) $this->setProp($name, $value);
  242. return $this;
  243. }
  244. if(!is_string($prop) || empty($prop)) return $this;
  245. if($prop[0] === '#')
  246. {
  247. $this->add($value, substr($prop, 1));
  248. return $this;
  249. }
  250. $this->onSetProp($prop, $value);
  251. return $this;
  252. }
  253. public function hasProp()
  254. {
  255. $names = func_get_args();
  256. if(empty($names)) return false;
  257. foreach($names as $name)
  258. {
  259. if(!$this->props->has($name)) return false;
  260. }
  261. return true;
  262. }
  263. /**
  264. * @param string|mixed[] $props
  265. * @param mixed $value
  266. */
  267. public function setDefaultProps($props, $value = null)
  268. {
  269. if(is_string($props)) $props = array($props => $value);
  270. if(!is_array($props) || empty($props)) return;
  271. foreach($props as $name => $value)
  272. {
  273. if($this->props->isset($name)) continue;
  274. $this->setProp($name, $value);
  275. }
  276. }
  277. public function getRestProps()
  278. {
  279. return $this->props->skip(array_keys(static::definedPropsList()));
  280. }
  281. public function getDefinedProps()
  282. {
  283. return $this->props->pick(array_keys(static::definedPropsList()));
  284. }
  285. /**
  286. * @param mixed $item
  287. * @param string $blockName
  288. * @param bool $prepend
  289. */
  290. public function add($item, $blockName = 'children', $prepend = false)
  291. {
  292. if($item === null || is_bool($item)) return;
  293. if($item instanceof \Closure) $item = $item();
  294. if(is_array($item))
  295. {
  296. foreach($item as $child) $this->add($child, $blockName, $prepend);
  297. return;
  298. }
  299. if(isDirective($item)) $this->directive($item, $blockName);
  300. else $this->addToBlock($blockName, $item, $prepend);
  301. }
  302. /**
  303. * @param mixed $child
  304. * @param string $name
  305. * @param bool $prepend
  306. */
  307. public function addToBlock($name, $child, $prepend = false)
  308. {
  309. if($child === null || is_bool($child)) return;
  310. if(is_array($child))
  311. {
  312. foreach($child as $blockChild)
  313. {
  314. $this->addToBlock($name, $blockChild, $prepend);
  315. }
  316. return;
  317. }
  318. if($child instanceof node) $child->parent = $this;
  319. if($child instanceof node)
  320. {
  321. if(isset($child->parent)) skipRenderInGlobal($child);
  322. $child->parent = $this;
  323. }
  324. if($name === 'children' && $child instanceof node)
  325. {
  326. $blockName = static::getNameFromBlockMap($child->fullType());
  327. if($blockName !== null) $name = $blockName;
  328. }
  329. elseif(is_string($child))
  330. {
  331. /* Encode html special chars. */
  332. $child = htmlspecialchars(strval($child), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, null, false);
  333. }
  334. $result = $name === 'children' ? $this->onAddChild($child) : $this->onAddBlock($child, $name);
  335. if($result === false) return;
  336. if($result !== null && $result !== true) $child = $result;
  337. if(isset($this->blocks[$name]))
  338. {
  339. if($prepend) array_unshift($this->blocks[$name], $child);
  340. else $this->blocks[$name][] = $child;
  341. }
  342. else
  343. {
  344. $this->blocks[$name] = array($child);
  345. }
  346. }
  347. /**
  348. * @param \zin\iDirective $directive
  349. * @param string $blockName
  350. */
  351. public function directive($directive, $blockName = 'children')
  352. {
  353. if(!isset($directive->parent) || !$directive->parent) $directive->parent = $this;
  354. $directive->apply($this, $blockName);
  355. }
  356. /**
  357. * @param mixed $child
  358. */
  359. public function addChild($child)
  360. {
  361. return $this->addToBlock('children', $child);
  362. }
  363. public function remove()
  364. {
  365. $this->removed = true;
  366. }
  367. /**
  368. * @param string|null $blockName
  369. */
  370. public function empty($blockName = null)
  371. {
  372. if($blockName) unset($this->blocks[$blockName]);
  373. else $this->blocks = array();
  374. $this->removeBuildData('all');
  375. }
  376. /**
  377. * @param object|mixed[] $info
  378. * @param string $event
  379. */
  380. public function bindEvent($event, $info)
  381. {
  382. if($info instanceof jsCallback)
  383. {
  384. $this->props->bindEvent($event, $info->toJS());
  385. $this->removeBuildData();
  386. return;
  387. }
  388. if(is_array($info)) $info = (object)$info;
  389. if(!isset($this->eventBindings[$event])) $this->eventBindings[$event] = array();
  390. $this->eventBindings[$event][] = $info;
  391. if(!$this->hasProp('id')) $this->setProp('id', $this->gid);
  392. }
  393. public function buildEvents()
  394. {
  395. $events = $this->eventBindings;
  396. if(empty($events)) return null;
  397. $id = $this->id();
  398. $code = array
  399. (
  400. '$(function(){',
  401. $this->type() === 'html' ? 'const ele = document;' : 'const ele = document.getElementById("' . (empty($id) ? $this->gid : $id) . '");if(!ele)return;const $ele = $(ele); const events = new Set(($ele.attr("data-zin-events") || "").split(" ").filter(Boolean));'
  402. );
  403. foreach($events as $event => $bindingList)
  404. {
  405. $code[] = "\$ele.on('$event.zin.on', function(e){";
  406. foreach($bindingList as $binding)
  407. {
  408. if(is_string($binding)) $binding = (object)array('handler' => $binding);
  409. $selector = isset($binding->selector) ? $binding->selector : null;
  410. $handler = isset($binding->handler) ? trim($binding->handler) : '';
  411. $stop = isset($binding->stop) ? $binding->stop : null;
  412. $prevent = isset($binding->prevent) ? $binding->prevent : null;
  413. $self = isset($binding->self) ? $binding->self : null;
  414. $code[] = '(function(){';
  415. if($selector) $code[] = "const target = e.target.closest('$selector');if(!target) return;";
  416. else $code[] = "const target = ele;";
  417. if($self) $code[] = "if(ele !== e.target) return;";
  418. if($stop) $code[] = "e.stopPropagation();";
  419. if($prevent) $code[] = "e.preventDefault();";
  420. if(preg_match('/^[$A-Z_][0-9A-Z_$\[\]."\']*$/i', $handler)) $code[] = "($handler).call(target,e);";
  421. else $code[] = $handler;
  422. $code[] = '})();';
  423. }
  424. $code[] = "});events.add('$event');";
  425. }
  426. $code[] = '$ele.attr("data-zin-events", Array.from(events).join(" "));';
  427. $code[] = '});';
  428. return implode("\n", $code);
  429. }
  430. public function render()
  431. {
  432. if($this->removed) return '';
  433. $data = new stdClass();
  434. $data->html = renderToHtml(...$this->buildAll());
  435. context()->handleRenderNode($data, $this);
  436. return $data->html;
  437. }
  438. public function renderInner()
  439. {
  440. if($this->removed) return '';
  441. return renderToHtml(...$this->children());
  442. }
  443. /**
  444. * @param string $type
  445. */
  446. public function removeBuildData($type = 'children')
  447. {
  448. if(!$this->buildData) return;
  449. if($type === 'all')
  450. {
  451. $this->buildData = new stdClass();
  452. }
  453. elseif($type === 'before')
  454. {
  455. unset($this->buildData->before);
  456. }
  457. elseif($type === 'after')
  458. {
  459. unset($this->buildData->after);
  460. }
  461. else
  462. {
  463. unset($this->buildData->content);
  464. unset($this->buildData->children);
  465. }
  466. }
  467. /**
  468. * @param bool $force
  469. */
  470. public function prebuild($force = false)
  471. {
  472. $firstBuild = ($this->buildData === null || $force);
  473. if($firstBuild)
  474. {
  475. $context = context();
  476. $context->handleBeforeBuildNode($this);
  477. $data = new stdClass();
  478. $data->before = prebuild($this->buildBefore(), $this);
  479. $data->children = prebuild($this->children(), $this);
  480. $data->content = prebuild($this->buildContents(), $this);
  481. $data->after = prebuild($this->buildAfter(), $this);
  482. $this->buildData = $data;
  483. $context->handleBuildNode($data, $this);
  484. }
  485. else
  486. {
  487. if(!isset($this->buildData->before)) $this->buildData->before = prebuild($this->buildBefore(), $this);
  488. if(!isset($this->buildData->children)) $this->buildData->children = prebuild($this->children(), $this);
  489. if(!isset($this->buildData->content)) $this->buildData->content = prebuild($this->buildContents(), $this);
  490. if(!isset($this->buildData->after)) $this->buildData->after = prebuild($this->buildAfter(), $this);
  491. }
  492. return $this->buildData;
  493. }
  494. public function buildContents()
  495. {
  496. $content = $this->build();
  497. if(is_null($content) || is_bool($content)) $content = array();
  498. elseif(!is_array($content)) $content = array($content);
  499. return $content;
  500. }
  501. public function buildAll()
  502. {
  503. if($this->replacedWith !== null) return $this->replacedWith;
  504. $data = $this->prebuild();
  505. return array_merge($data->before, $data->content, $data->after);
  506. }
  507. /**
  508. * @param mixed ...$args
  509. */
  510. public function replaceWith(...$args)
  511. {
  512. $this->replacedWith = $args;
  513. }
  514. public function children()
  515. {
  516. return $this->block('children');
  517. }
  518. /**
  519. * @param string $name
  520. */
  521. public function block($name)
  522. {
  523. $list = array();
  524. if(isset($this->blocks[$name]))
  525. {
  526. $items = $this->blocks[$name];
  527. foreach($items as $item)
  528. {
  529. if(is_array($item)) $list = array_merge($list, $item);
  530. else $list[] = $item;
  531. }
  532. }
  533. return $list;
  534. }
  535. /**
  536. * @param string $name
  537. */
  538. public function hasBlock($name)
  539. {
  540. return isset($this->blocks[$name]);
  541. }
  542. /**
  543. * Convert to JSON object.
  544. *
  545. * @access public
  546. * @return object
  547. */
  548. public function toJSON()
  549. {
  550. $json = new stdClass();
  551. $json->gid = $this->gid;
  552. $json->type = $this->type();
  553. $json->props = $this->props->toJSON();
  554. $json->blocks = array();
  555. foreach($this->blocks as $key => $block)
  556. {
  557. foreach($block as $index => $child)
  558. {
  559. if($child instanceof node || (is_object($child) && method_exists($child, 'toJSON')))
  560. {
  561. $block[$index] = $child->toJSON();
  562. }
  563. }
  564. if($key === 'children')
  565. {
  566. $json->$key = $block;
  567. unset($json->blocks[$key]);
  568. }
  569. else
  570. {
  571. $json->blocks[$key] = $block;
  572. }
  573. }
  574. if(!$json->blocks) unset($json->blocks);
  575. $id = $this->id();
  576. if($id !== null) $json->id = $id;
  577. $parent = $this->parent;
  578. if($parent !== null) $json->parent = $parent->displayID();
  579. if($this->removed) $json->removed = true;
  580. return $json;
  581. }
  582. /**
  583. * Serialized to JSON string.
  584. *
  585. * @access public
  586. * @return mixed
  587. */
  588. #[\ReturnTypeWillChange]
  589. public function jsonSerialize()
  590. {
  591. return json_encode($this->toJSON());
  592. }
  593. /**
  594. * Trigger error in debug mode.
  595. *
  596. * @access public
  597. * @param string $message
  598. * @param int $level
  599. * @return void
  600. */
  601. public function triggerError($message, $level = E_USER_ERROR)
  602. {
  603. triggerError("{$this->displayID()}: $message", $level);
  604. }
  605. protected function build()
  606. {
  607. return $this->children();
  608. }
  609. protected function buildBefore()
  610. {
  611. return $this->block('before');
  612. }
  613. protected function buildAfter()
  614. {
  615. return $this->block('after');
  616. }
  617. protected function created()
  618. {
  619. }
  620. /**
  621. * @param mixed $child
  622. */
  623. protected function onAddChild($child)
  624. {
  625. $this->removeBuildData();
  626. return $child;
  627. }
  628. /**
  629. * @param mixed $child
  630. * @param string $name
  631. */
  632. protected function onAddBlock($child, $name)
  633. {
  634. $this->removeBuildData($name);
  635. return $child;
  636. }
  637. /**
  638. * @param mixed[]|string $prop
  639. * @param mixed $value
  640. */
  641. protected function onSetProp($prop, $value)
  642. {
  643. if($prop === 'id' && $value === '$GID') $value = $this->gid;
  644. $this->props->set($prop, $value);
  645. $this->removeBuildData();
  646. }
  647. /**
  648. * @param mixed $defaultValue
  649. * @return mixed
  650. * @param string $prop
  651. */
  652. protected function onGetProp($prop, $defaultValue)
  653. {
  654. return $this->props->get($prop, $defaultValue);
  655. }
  656. /**
  657. * Check errors in debug mode.
  658. *
  659. * @access protected
  660. * @return void
  661. */
  662. protected function checkErrors()
  663. {
  664. if(!isDebug()) return;
  665. $definedProps = static::definedPropsList();
  666. foreach($definedProps as $name => $definition)
  667. {
  668. if($this->hasProp($name)) continue;
  669. if(isset($definition['default']) && $definition['default'] !== null) continue;
  670. if(isset($definition['optional']) && $definition['optional']) continue;
  671. $this->triggerError("The value of property \"$name: {$definition['type']}\" is required.");
  672. }
  673. $wgErrors = $this->onCheckErrors();
  674. if(empty($wgErrors)) return;
  675. foreach($wgErrors as $error)
  676. {
  677. if(is_array($error)) $this->triggerError(...$error);
  678. else $this->triggerError($error);
  679. }
  680. }
  681. /**
  682. * The lifecycle method for checking errors in debug mode.
  683. *
  684. * @access protected
  685. * @return array|null
  686. */
  687. protected function onCheckErrors()
  688. {
  689. return null;
  690. }
  691. /**
  692. * @var mixed[]
  693. */
  694. protected static $definedPropsMap = array();
  695. /**
  696. * @var mixed[]
  697. */
  698. protected static $blockMap = array();
  699. /**
  700. * @var mixed[]
  701. */
  702. protected static $gidMap = array();
  703. /**
  704. * @var string|null
  705. */
  706. protected static $pageKey;
  707. public static function nextGid($prefix = 'zin_', $type = null)
  708. {
  709. global $config;
  710. if(!isset($config->clientCache) || !$config->clientCache)
  711. {
  712. return $prefix . uniqid();
  713. }
  714. if($type === null)
  715. {
  716. $type = get_called_class();
  717. if(str_starts_with($type, 'zin\\')) $type = substr($type, 4);
  718. }
  719. $lastID = isset(static::$gidMap[$type]) ? static::$gidMap[$type] : -1;
  720. $nextID = $lastID + 1;
  721. static::$gidMap[$type] = $nextID;
  722. $key = static::$pageKey;
  723. if($key === null)
  724. {
  725. global $app;
  726. $key = $app->rawModule . '_' . $app->rawMethod . '_';
  727. $pageDataID = data($app->rawModule . 'ID');
  728. if(!$pageDataID)
  729. {
  730. $pageData = data($app->rawModule);
  731. if(is_object($pageData) && isset($pageData->id)) $pageDataID = $pageData->id;
  732. elseif(is_array($pageData) && isset($pageData['id'])) $pageDataID = $pageData['id'];
  733. }
  734. if($pageDataID) $key .= $pageDataID . '_';
  735. static::$pageKey = $key;
  736. }
  737. $id = $prefix . $key . $type;
  738. if($nextID > 0) $id .= "_$nextID";
  739. return $id;
  740. }
  741. public static function getBlockMap()
  742. {
  743. $type = get_called_class();
  744. if(!isset(node::$blockMap[$type]))
  745. {
  746. $blockMap = array();
  747. if(is_array(static::$defineBlocks))
  748. {
  749. foreach(static::$defineBlocks as $blockName => $setting)
  750. {
  751. if(!isset($setting['map'])) continue;
  752. $map = $setting['map'];
  753. if(is_string($map)) $map = explode(',', $map);
  754. foreach($map as $name) $blockMap[$name] = $blockName;
  755. }
  756. }
  757. node::$blockMap[$type] = $blockMap;
  758. }
  759. return node::$blockMap[$type];
  760. }
  761. /**
  762. * @param string $type
  763. */
  764. public static function getNameFromBlockMap($type)
  765. {
  766. $blockMap = static::getBlockMap();
  767. if(str_starts_with($type, 'zin\\')) $type = substr($type, 4);
  768. return isset($blockMap[$type]) ? $blockMap[$type] : null;
  769. }
  770. /**
  771. * @param string|null $type
  772. */
  773. public static function definedPropsList($type = null)
  774. {
  775. if($type === null) $type = get_called_class();
  776. if(!isset(node::$definedPropsMap[$type]) && $type === get_called_class())
  777. {
  778. node::$definedPropsMap[$type] = static::parsePropsDefinition();
  779. }
  780. return node::$definedPropsMap[$type];
  781. }
  782. /**
  783. * @param string|null $type
  784. */
  785. public static function getDefaultProps($type = null)
  786. {
  787. $type = $type ? $type : get_called_class();
  788. $defaultProps = array();
  789. $definedPropsList = static::definedPropsList($type);
  790. foreach($definedPropsList as $name => $definition)
  791. {
  792. if(!isset($definition['default'])) continue;
  793. $defaultProps[$name] = $definition['default'];
  794. }
  795. return $defaultProps;
  796. }
  797. /**
  798. * Parse props definition
  799. * @param $definition
  800. * @example
  801. *
  802. * $definition = array('name', 'desc:string', 'title?:string|array', 'icon?:string="star"');
  803. * $definition = array('name' => 'mixed', 'desc' => '?string', 'title' => array('type' => 'string|array', 'optional' => true), 'icon' => array('type' => 'string', 'default' => 'star', 'optional' => true))))
  804. */
  805. protected static function parsePropsDefinition()
  806. {
  807. $parentClass = get_parent_class(get_called_class());
  808. $parentProps = array();
  809. $defaultProps = static::$defaultProps;
  810. $definition = static::$defineProps;
  811. if($parentClass)
  812. {
  813. if($definition === $parentClass::$defineProps) $definition = array();
  814. if($defaultProps === $parentClass::$defaultProps) $defaultProps = array();
  815. $parentProps = call_user_func("$parentClass::definedPropsList", $parentClass);
  816. }
  817. return parsePropsMap($definition, $parentProps, $defaultProps);
  818. }
  819. }
  820. /**
  821. * @param \zin\node|mixed[] $list
  822. */
  823. function findInNode($selectors, $list, $first = false, $reverse = false, $onlyContent = true)
  824. {
  825. if($list instanceof node)
  826. {
  827. $data = $list->buildData;
  828. if(!$data) return array();
  829. $list = $data->content ? $data->content : array();
  830. if(!$onlyContent) $list = array_merge($data->before ? $data->before : array(), $list, $data->after ? $data->after : array());
  831. }
  832. if($reverse) $list = array_reverse($list);
  833. $result = array();
  834. foreach($list as $child)
  835. {
  836. if(is_array($child))
  837. {
  838. $childList = findInNode($selectors, $child, $first, $reverse, $onlyContent);
  839. if(!empty($childList))
  840. {
  841. if($first) return $childList;
  842. $result = array_merge($result, $childList);
  843. }
  844. continue;
  845. }
  846. if(!($child instanceof node)) continue;
  847. if($child->is($selectors) && $child->type() !== 'item')
  848. {
  849. if($child->parent && ($child->parent->removed || ($child->parent instanceof wg && $child->parent->type() !== 'item' && $child->parent->is($selectors)))) continue;
  850. $result[$child->gid] = $child;
  851. if($first) return $result;
  852. }
  853. $childList = findInNode($selectors, $child, $first, $reverse, $onlyContent);
  854. if(!empty($childList))
  855. {
  856. if($first) return $childList;
  857. $result = array_merge($result, $childList);
  858. }
  859. }
  860. return $result;
  861. }
  862. /**
  863. * @param mixed ...$items
  864. */
  865. function renderToHtml(...$items)
  866. {
  867. $html = '';
  868. foreach($items as $item)
  869. {
  870. if(is_array($item))
  871. {
  872. $html .= renderToHtml(...$item);
  873. continue;
  874. }
  875. if($item instanceof node || (is_object($item) && method_exists($item, 'render')))
  876. {
  877. $html .= $item->render();
  878. continue;
  879. }
  880. if(is_object($item) && isset($item->html))
  881. {
  882. $html .= $item->html;
  883. continue;
  884. }
  885. if(!is_string($item)) $item = strval($item);
  886. $html .= strval($item);
  887. }
  888. return $html;
  889. }
  890. function prebuild($items, $parent = null)
  891. {
  892. foreach($items as $index => $item)
  893. {
  894. if(is_array($item))
  895. {
  896. $items[$index] = prebuild($item, $parent);
  897. continue;
  898. }
  899. if(!($item instanceof node)) continue;
  900. if($parent) $item->parent = $parent;
  901. $item->prebuild();
  902. }
  903. return $items;
  904. }
  905. /**
  906. * Parse the props definition.
  907. *
  908. * @param array $definition - The props definition.
  909. * @param array $parentProps - The parent props.
  910. * @param array $defaultValues - The default values.
  911. * @return array
  912. */
  913. function parsePropsMap($definition, $parentProps = array(), $defaultValues = array())
  914. {
  915. $props = $parentProps;
  916. foreach($parentProps as $parentProp)
  917. {
  918. $name = $parentProp['name'];
  919. if(isset($defaultValues[$name])) $parentProp['default'] = $defaultValues[$name];
  920. $props[$name] = $parentProp;
  921. }
  922. foreach($definition as $name => $value)
  923. {
  924. $prop = parseProp($value, is_string($name) ? $name : null);
  925. $name = $prop['name'];
  926. if(isset($defaultValues[$name]))
  927. {
  928. $prop['default'] = $defaultValues[$name];
  929. }
  930. elseif(!isset($prop['default']) && isset($parentProps[$name]['default']) && $parentProps[$name]['default'])
  931. {
  932. $prop['default'] = $parentProps[$name]['default'];
  933. }
  934. $props[$name] = $prop;
  935. }
  936. return $props;
  937. }
  938. /**
  939. * Parse the prop definition.
  940. *
  941. * @param string|array $definition - The prop definition.
  942. * @param string|null $name - The prop name.
  943. * @return array
  944. */
  945. function parseProp($definition, $name = null)
  946. {
  947. $optional = false;
  948. $type = 'mixed';
  949. $prop = array();
  950. if(is_string($definition)) $definition = trim($definition);
  951. /* Parse definition like `'name?: type1|type2="default"'` . */
  952. if(!$name && is_string($definition))
  953. {
  954. if(str_contains($definition, ':'))
  955. {
  956. list($name, $definition) = explode(':', $definition, 2);
  957. }
  958. else
  959. {
  960. $name = $definition;
  961. $definition = '';
  962. }
  963. $name = trim($name);
  964. if(str_ends_with($name, '?'))
  965. {
  966. $name = substr($name, 0, strlen($name) - 1);
  967. $optional = true;
  968. }
  969. }
  970. /* Parse definition like `'name' => '?type1|type2="default"'` . */
  971. if(is_array($definition))
  972. {
  973. if(isset($definition['type'])) $type = $definition['type'];
  974. if(isset($definition['default'])) $prop['default'] = $definition['default'];
  975. if(isset($definition['optional'])) $optional = $definition['optional'];
  976. }
  977. else if(is_string($definition))
  978. {
  979. if(str_contains($definition, '='))
  980. {
  981. list($type, $default) = explode('=', $definition, 2);
  982. if(strlen($default)) $prop['default'] = json_decode(trim($default));
  983. }
  984. else
  985. {
  986. $type = $definition;
  987. }
  988. }
  989. $type = trim($type);
  990. if(str_starts_with($type, '?'))
  991. {
  992. $type = substr($type, 1);
  993. $optional = true;
  994. }
  995. $typeList = explode('|', $type);
  996. if(in_array('null', $typeList) || in_array('mixed', $typeList))
  997. {
  998. $optional = true;
  999. }
  1000. elseif($optional)
  1001. {
  1002. array_unshift($typeList, 'null');
  1003. }
  1004. $prop['name'] = $name;
  1005. $prop['type'] = implode('|', $typeList);
  1006. $prop['optional'] = $optional || (isset($prop['default']) && $prop['default'] !== null);
  1007. return $prop;
  1008. }