1015 lines
29 KiB
PHP
1015 lines
29 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
/**
|
|
* The base node class file of zin of ZenTaoPMS.
|
|
*
|
|
* @copyright Copyright 2023 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
|
|
* @author Hao Sun <sunhao@easycorp.ltd>
|
|
* @package zin
|
|
* @version $Id
|
|
* @link https://www.zentao.net
|
|
*/
|
|
|
|
namespace zin;
|
|
|
|
require_once __DIR__ . DS . 'helper.func.php';
|
|
require_once __DIR__ . DS . 'selector.func.php';
|
|
require_once __DIR__ . DS . 'props.class.php';
|
|
|
|
/**
|
|
* The base node class.
|
|
*/
|
|
class node implements \JsonSerializable
|
|
{
|
|
/**
|
|
* Define properties
|
|
*
|
|
* @access public
|
|
* @var array
|
|
*/
|
|
protected static array $defineProps = array();
|
|
|
|
/**
|
|
* Default properties
|
|
*
|
|
* @access public
|
|
* @var array
|
|
*/
|
|
protected static array $defaultProps = array();
|
|
|
|
protected static array $defineBlocks = array();
|
|
|
|
public string $gid;
|
|
|
|
public ?node $parent = null;
|
|
|
|
public ?node $rootNode = null;
|
|
|
|
public props $props;
|
|
|
|
public array $blocks = array();
|
|
|
|
public bool $removed = false;
|
|
|
|
public ?array $replacedWith = null;
|
|
|
|
public ?stdClass $buildData = null;
|
|
|
|
public array $eventBindings = array();
|
|
|
|
public ?bool $notRenderInGlobal = null;
|
|
|
|
public function __construct(mixed ...$args)
|
|
{
|
|
$this->gid = static::nextGid();
|
|
$this->props = new props();
|
|
|
|
disableGlobalRender();
|
|
|
|
$this->setDefaultProps(static::getDefaultProps());
|
|
$this->add($args);
|
|
$this->created();
|
|
|
|
enableGlobalRender();
|
|
renderInGlobal($this);
|
|
}
|
|
|
|
public function __debugInfo(): array
|
|
{
|
|
return (array)$this->toJSON();
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return $this->render();
|
|
}
|
|
|
|
public function fullType(): string
|
|
{
|
|
return get_called_class();
|
|
}
|
|
|
|
public function type(): string
|
|
{
|
|
$type = $this->fullType();
|
|
if(str_contains($type, '\\'))
|
|
{
|
|
$type = substr($type, strrpos($type, '\\') + 1);
|
|
}
|
|
return $type;
|
|
}
|
|
|
|
public function id(): ?string
|
|
{
|
|
return strval($this->props->get('id'));
|
|
}
|
|
|
|
public function displayID(): string
|
|
{
|
|
$displayID = $this->fullType() . '~' . $this->gid;
|
|
|
|
$id = $this->id();
|
|
if(!empty($id)) $displayID .= "#$id";
|
|
|
|
return $displayID;
|
|
}
|
|
|
|
/**
|
|
* Check if the element is match any of the selectors
|
|
* @param string|array|object $selectors
|
|
*/
|
|
public function is(string|array|object $selectors): bool
|
|
{
|
|
$list = parseSelectors($selectors);
|
|
foreach($list as $selector)
|
|
{
|
|
if($this->isMatch($selector)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function isMatch(object $selector): bool
|
|
{
|
|
if(!empty($selector->id) && $this->id() !== $selector->id) return false;
|
|
if(!empty($selector->tag) && $this->type() !== $selector->tag) return false;
|
|
if(!empty($selector->class) && !$this->props->class->has($selector->class)) return false;
|
|
if(!empty($selector->parents) && !$this->hasParents(...$selector->parents)) return false;
|
|
return true;
|
|
}
|
|
|
|
public function hasParents(object|string ...$parentSelectors): bool
|
|
{
|
|
$parent = $this->parent;
|
|
if(!$parent) return false;
|
|
|
|
foreach($parentSelectors as $selector)
|
|
{
|
|
$parent = $parent->closest($selector);
|
|
if(!$parent) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function off(string $event)
|
|
{
|
|
unset($this->eventBindings[$event]);
|
|
$this->props->remove("@$event");
|
|
}
|
|
|
|
public function closest(string|array|object $selectors): ?node
|
|
{
|
|
$list = parseSelectors($selectors, true);
|
|
$node = $this;
|
|
while($node)
|
|
{
|
|
if($node->is($list)) return $node;
|
|
$node = $node->parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function find(string|array|object $selectors, bool $first = false, bool $reverse = false, bool $prebuild = true): array
|
|
{
|
|
if($prebuild) $this->prebuild();
|
|
return findInNode(parseSelectors($selectors, true), $this, $first, $reverse);
|
|
}
|
|
|
|
public function findFirst(string|array|object $selectors): ?node
|
|
{
|
|
$results = $this->find($selectors, true);
|
|
return empty($results) ? null : reset($results);
|
|
}
|
|
|
|
public function findLast(string|array|object $selectors): ?node
|
|
{
|
|
$results = $this->find($selectors, true, true);
|
|
return empty($results) ? null : end($results);
|
|
}
|
|
|
|
public function prop(array|string $name, mixed $defaultValue = null): mixed
|
|
{
|
|
if(is_array($name))
|
|
{
|
|
$values = array();
|
|
foreach($name as $index => $propName)
|
|
{
|
|
$values[] = $this->onGetProp($propName, is_array($defaultValue) ? (isset($defaultValue[$propName]) ? $defaultValue[$propName] : $defaultValue[$index]) : $defaultValue);
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
return $this->onGetProp($name, $defaultValue);
|
|
}
|
|
|
|
/**
|
|
* Set property, an array can be passed to set multiple properties
|
|
*
|
|
* @access public
|
|
* @param props|array|string $prop - Property name or properties list
|
|
* @param mixed $value - Property value
|
|
*/
|
|
public function setProp(props|array|string $prop, mixed $value = null)
|
|
{
|
|
if($prop instanceof props) $prop = $prop->toJSON();
|
|
|
|
if(is_array($prop))
|
|
{
|
|
foreach($prop as $name => $value) $this->setProp($name, $value);
|
|
return $this;
|
|
}
|
|
|
|
if(!is_string($prop) || empty($prop)) return $this;
|
|
|
|
if($prop[0] === '#')
|
|
{
|
|
$this->add($value, substr($prop, 1));
|
|
return $this;
|
|
}
|
|
|
|
$this->onSetProp($prop, $value);
|
|
return $this;
|
|
}
|
|
|
|
public function hasProp(): bool
|
|
{
|
|
$names = func_get_args();
|
|
if(empty($names)) return false;
|
|
foreach($names as $name)
|
|
{
|
|
if(!$this->props->has($name)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function setDefaultProps(string|array $props, mixed $value = null)
|
|
{
|
|
if(is_string($props)) $props = array($props => $value);
|
|
if(!is_array($props) || empty($props)) return;
|
|
|
|
foreach($props as $name => $value)
|
|
{
|
|
if($this->props->isset($name)) continue;
|
|
$this->setProp($name, $value);
|
|
}
|
|
}
|
|
|
|
public function getRestProps(): array
|
|
{
|
|
return $this->props->skip(array_keys(static::definedPropsList()));
|
|
}
|
|
|
|
public function getDefinedProps(): array
|
|
{
|
|
return $this->props->pick(array_keys(static::definedPropsList()));
|
|
}
|
|
|
|
public function add(mixed $item, string $blockName = 'children', bool $prepend = false)
|
|
{
|
|
if($item === null || is_bool($item)) return;
|
|
|
|
if($item instanceof \Closure) $item = $item();
|
|
if(is_array($item))
|
|
{
|
|
foreach($item as $child) $this->add($child, $blockName, $prepend);
|
|
return;
|
|
}
|
|
|
|
if(isDirective($item)) $this->directive($item, $blockName);
|
|
else $this->addToBlock($blockName, $item, $prepend);
|
|
}
|
|
|
|
public function addToBlock(string $name, mixed $child, bool $prepend = false)
|
|
{
|
|
if($child === null || is_bool($child)) return;
|
|
|
|
if(is_array($child))
|
|
{
|
|
foreach($child as $blockChild)
|
|
{
|
|
$this->addToBlock($name, $blockChild, $prepend);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if($child instanceof node) $child->parent = $this;
|
|
|
|
if($child instanceof node)
|
|
{
|
|
if(isset($child->parent)) skipRenderInGlobal($child);
|
|
$child->parent = $this;
|
|
}
|
|
|
|
if($name === 'children' && $child instanceof node)
|
|
{
|
|
$blockName = static::getNameFromBlockMap($child->fullType());
|
|
if($blockName !== null) $name = $blockName;
|
|
}
|
|
elseif(is_string($child))
|
|
{
|
|
/* Encode html special chars. */
|
|
$child = htmlspecialchars(strval($child), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, null, false);
|
|
}
|
|
|
|
$result = $name === 'children' ? $this->onAddChild($child) : $this->onAddBlock($child, $name);
|
|
if($result === false) return;
|
|
if($result !== null && $result !== true) $child = $result;
|
|
|
|
if(isset($this->blocks[$name]))
|
|
{
|
|
if($prepend) array_unshift($this->blocks[$name], $child);
|
|
else $this->blocks[$name][] = $child;
|
|
}
|
|
else
|
|
{
|
|
$this->blocks[$name] = array($child);
|
|
}
|
|
}
|
|
|
|
public function directive(iDirective $directive, string $blockName = 'children')
|
|
{
|
|
if(!isset($directive->parent) || !$directive->parent) $directive->parent = $this;
|
|
$directive->apply($this, $blockName);
|
|
}
|
|
|
|
public function addChild(mixed $child)
|
|
{
|
|
return $this->addToBlock('children', $child);
|
|
}
|
|
|
|
public function remove()
|
|
{
|
|
$this->removed = true;
|
|
}
|
|
|
|
public function empty(?string $blockName = null)
|
|
{
|
|
if($blockName) unset($this->blocks[$blockName]);
|
|
else $this->blocks = array();
|
|
|
|
$this->removeBuildData('all');
|
|
}
|
|
|
|
public function bindEvent(string $event, object|array $info)
|
|
{
|
|
if($info instanceof jsCallback)
|
|
{
|
|
$this->props->bindEvent($event, $info->toJS());
|
|
$this->removeBuildData();
|
|
return;
|
|
}
|
|
|
|
if(is_array($info)) $info = (object)$info;
|
|
if(!isset($this->eventBindings[$event])) $this->eventBindings[$event] = array();
|
|
$this->eventBindings[$event][] = $info;
|
|
|
|
if(!$this->hasProp('id')) $this->setProp('id', $this->gid);
|
|
}
|
|
|
|
public function buildEvents(): ?string
|
|
{
|
|
$events = $this->eventBindings;
|
|
if(empty($events)) return null;
|
|
|
|
$id = $this->id();
|
|
$code = array
|
|
(
|
|
'$(function(){',
|
|
$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));'
|
|
);
|
|
foreach($events as $event => $bindingList)
|
|
{
|
|
$code[] = "\$ele.on('$event.zin.on', function(e){";
|
|
foreach($bindingList as $binding)
|
|
{
|
|
if(is_string($binding)) $binding = (object)array('handler' => $binding);
|
|
$selector = isset($binding->selector) ? $binding->selector : null;
|
|
$handler = isset($binding->handler) ? trim($binding->handler) : '';
|
|
$stop = isset($binding->stop) ? $binding->stop : null;
|
|
$prevent = isset($binding->prevent) ? $binding->prevent : null;
|
|
$self = isset($binding->self) ? $binding->self : null;
|
|
|
|
$code[] = '(function(){';
|
|
if($selector) $code[] = "const target = e.target.closest('$selector');if(!target) return;";
|
|
else $code[] = "const target = ele;";
|
|
if($self) $code[] = "if(ele !== e.target) return;";
|
|
if($stop) $code[] = "e.stopPropagation();";
|
|
if($prevent) $code[] = "e.preventDefault();";
|
|
|
|
if(preg_match('/^[$A-Z_][0-9A-Z_$\[\]."\']*$/i', $handler)) $code[] = "($handler).call(target,e);";
|
|
else $code[] = $handler;
|
|
|
|
$code[] = '})();';
|
|
}
|
|
$code[] = "});events.add('$event');";
|
|
}
|
|
$code[] = '$ele.attr("data-zin-events", Array.from(events).join(" "));';
|
|
$code[] = '});';
|
|
return implode("\n", $code);
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
if($this->removed) return '';
|
|
|
|
$data = new stdClass();
|
|
$data->html = renderToHtml(...$this->buildAll());
|
|
|
|
context()->handleRenderNode($data, $this);
|
|
|
|
return $data->html;
|
|
}
|
|
|
|
public function renderInner(): string
|
|
{
|
|
if($this->removed) return '';
|
|
|
|
return renderToHtml(...$this->children());
|
|
}
|
|
|
|
public function removeBuildData(string $type = 'children')
|
|
{
|
|
if(!$this->buildData) return;
|
|
|
|
if($type === 'all')
|
|
{
|
|
$this->buildData = new stdClass();
|
|
}
|
|
elseif($type === 'before')
|
|
{
|
|
unset($this->buildData->before);
|
|
}
|
|
elseif($type === 'after')
|
|
{
|
|
unset($this->buildData->after);
|
|
}
|
|
else
|
|
{
|
|
unset($this->buildData->content);
|
|
unset($this->buildData->children);
|
|
}
|
|
}
|
|
|
|
public function prebuild(bool $force = false): stdClass
|
|
{
|
|
$firstBuild = ($this->buildData === null || $force);
|
|
if($firstBuild)
|
|
{
|
|
$context = context();
|
|
$context->handleBeforeBuildNode($this);
|
|
|
|
$data = new stdClass();
|
|
$data->before = prebuild($this->buildBefore(), $this);
|
|
$data->children = prebuild($this->children(), $this);
|
|
$data->content = prebuild($this->buildContents(), $this);
|
|
$data->after = prebuild($this->buildAfter(), $this);
|
|
|
|
$this->buildData = $data;
|
|
|
|
$context->handleBuildNode($data, $this);
|
|
}
|
|
else
|
|
{
|
|
if(!isset($this->buildData->before)) $this->buildData->before = prebuild($this->buildBefore(), $this);
|
|
if(!isset($this->buildData->children)) $this->buildData->children = prebuild($this->children(), $this);
|
|
if(!isset($this->buildData->content)) $this->buildData->content = prebuild($this->buildContents(), $this);
|
|
if(!isset($this->buildData->after)) $this->buildData->after = prebuild($this->buildAfter(), $this);
|
|
}
|
|
|
|
return $this->buildData;
|
|
}
|
|
|
|
public function buildContents(): array
|
|
{
|
|
$content = $this->build();
|
|
if(is_null($content) || is_bool($content)) $content = array();
|
|
elseif(!is_array($content)) $content = array($content);
|
|
return $content;
|
|
}
|
|
|
|
public function buildAll(): array
|
|
{
|
|
if($this->replacedWith !== null) return $this->replacedWith;
|
|
|
|
$data = $this->prebuild();
|
|
return array_merge($data->before, $data->content, $data->after);
|
|
}
|
|
|
|
public function replaceWith(mixed ...$args)
|
|
{
|
|
$this->replacedWith = $args;
|
|
}
|
|
|
|
public function children(): array
|
|
{
|
|
return $this->block('children');
|
|
}
|
|
|
|
public function block(string $name): array
|
|
{
|
|
$list = array();
|
|
if(isset($this->blocks[$name]))
|
|
{
|
|
$items = $this->blocks[$name];
|
|
foreach($items as $item)
|
|
{
|
|
if(is_array($item)) $list = array_merge($list, $item);
|
|
else $list[] = $item;
|
|
}
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
public function hasBlock(string $name): bool
|
|
{
|
|
return isset($this->blocks[$name]);
|
|
}
|
|
|
|
/**
|
|
* Convert to JSON object.
|
|
*
|
|
* @access public
|
|
* @return object
|
|
*/
|
|
public function toJSON(): object
|
|
{
|
|
$json = new stdClass();
|
|
$json->gid = $this->gid;
|
|
$json->type = $this->type();
|
|
$json->props = $this->props->toJSON();
|
|
|
|
$json->blocks = array();
|
|
foreach($this->blocks as $key => $block)
|
|
{
|
|
foreach($block as $index => $child)
|
|
{
|
|
if($child instanceof node || (is_object($child) && method_exists($child, 'toJSON')))
|
|
{
|
|
$block[$index] = $child->toJSON();
|
|
}
|
|
}
|
|
|
|
if($key === 'children')
|
|
{
|
|
$json->$key = $block;
|
|
unset($json->blocks[$key]);
|
|
}
|
|
else
|
|
{
|
|
$json->blocks[$key] = $block;
|
|
}
|
|
}
|
|
|
|
if(!$json->blocks) unset($json->blocks);
|
|
|
|
$id = $this->id();
|
|
if($id !== null) $json->id = $id;
|
|
|
|
$parent = $this->parent;
|
|
if($parent !== null) $json->parent = $parent->displayID();
|
|
|
|
if($this->removed) $json->removed = true;
|
|
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* Serialized to JSON string.
|
|
*
|
|
* @access public
|
|
* @return mixed
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
public function jsonSerialize(): mixed
|
|
{
|
|
return json_encode($this->toJSON());
|
|
}
|
|
|
|
/**
|
|
* Trigger error in debug mode.
|
|
*
|
|
* @access public
|
|
* @param string $message
|
|
* @param int $level
|
|
* @return void
|
|
*/
|
|
public function triggerError(string $message, int $level = E_USER_ERROR)
|
|
{
|
|
triggerError("{$this->displayID()}: $message", $level);
|
|
}
|
|
|
|
protected function build()
|
|
{
|
|
return $this->children();
|
|
}
|
|
|
|
protected function buildBefore(): array
|
|
{
|
|
return $this->block('before');
|
|
}
|
|
|
|
protected function buildAfter(): array
|
|
{
|
|
return $this->block('after');
|
|
}
|
|
|
|
protected function created()
|
|
{
|
|
}
|
|
|
|
protected function onAddChild(mixed $child)
|
|
{
|
|
$this->removeBuildData();
|
|
return $child;
|
|
}
|
|
|
|
protected function onAddBlock(mixed $child, string $name)
|
|
{
|
|
$this->removeBuildData($name);
|
|
return $child;
|
|
}
|
|
|
|
protected function onSetProp(array|string $prop, mixed $value)
|
|
{
|
|
if($prop === 'id' && $value === '$GID') $value = $this->gid;
|
|
|
|
$this->props->set($prop, $value);
|
|
$this->removeBuildData();
|
|
}
|
|
|
|
protected function onGetProp(string $prop, mixed $defaultValue): mixed
|
|
{
|
|
return $this->props->get($prop, $defaultValue);
|
|
}
|
|
|
|
/**
|
|
* Check errors in debug mode.
|
|
*
|
|
* @access protected
|
|
* @return void
|
|
*/
|
|
protected function checkErrors()
|
|
{
|
|
if(!isDebug()) return;
|
|
|
|
$definedProps = static::definedPropsList();
|
|
foreach($definedProps as $name => $definition)
|
|
{
|
|
if($this->hasProp($name)) continue;
|
|
if(isset($definition['default']) && $definition['default'] !== null) continue;
|
|
if(isset($definition['optional']) && $definition['optional']) continue;
|
|
|
|
$this->triggerError("The value of property \"$name: {$definition['type']}\" is required.");
|
|
}
|
|
|
|
$wgErrors = $this->onCheckErrors();
|
|
if(empty($wgErrors)) return;
|
|
|
|
foreach($wgErrors as $error)
|
|
{
|
|
if(is_array($error)) $this->triggerError(...$error);
|
|
else $this->triggerError($error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The lifecycle method for checking errors in debug mode.
|
|
*
|
|
* @access protected
|
|
* @return array|null
|
|
*/
|
|
protected function onCheckErrors(): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
protected static array $definedPropsMap = array();
|
|
|
|
protected static array $blockMap = array();
|
|
|
|
protected static array $gidMap = array();
|
|
|
|
protected static ?string $pageKey = null;
|
|
|
|
public static function nextGid($prefix = 'zin_', $type = null): string
|
|
{
|
|
global $config;
|
|
if(!isset($config->clientCache) || !$config->clientCache)
|
|
{
|
|
return $prefix . uniqid();
|
|
}
|
|
|
|
if($type === null)
|
|
{
|
|
$type = get_called_class();
|
|
if(str_starts_with($type, 'zin\\')) $type = substr($type, 4);
|
|
}
|
|
$lastID = isset(static::$gidMap[$type]) ? static::$gidMap[$type] : -1;
|
|
$nextID = $lastID + 1;
|
|
static::$gidMap[$type] = $nextID;
|
|
|
|
$key = static::$pageKey;
|
|
if($key === null)
|
|
{
|
|
global $app;
|
|
$key = $app->rawModule . '_' . $app->rawMethod . '_';
|
|
$pageDataID = data($app->rawModule . 'ID');
|
|
if(!$pageDataID)
|
|
{
|
|
$pageData = data($app->rawModule);
|
|
if(is_object($pageData) && isset($pageData->id)) $pageDataID = $pageData->id;
|
|
elseif(is_array($pageData) && isset($pageData['id'])) $pageDataID = $pageData['id'];
|
|
}
|
|
if($pageDataID) $key .= $pageDataID . '_';
|
|
static::$pageKey = $key;
|
|
}
|
|
|
|
$id = $prefix . $key . $type;
|
|
if($nextID > 0) $id .= "_$nextID";
|
|
return $id;
|
|
}
|
|
|
|
public static function getBlockMap(): array
|
|
{
|
|
$type = get_called_class();
|
|
if(!isset(node::$blockMap[$type]))
|
|
{
|
|
$blockMap = array();
|
|
if(is_array(static::$defineBlocks))
|
|
{
|
|
foreach(static::$defineBlocks as $blockName => $setting)
|
|
{
|
|
if(!isset($setting['map'])) continue;
|
|
$map = $setting['map'];
|
|
if(is_string($map)) $map = explode(',', $map);
|
|
foreach($map as $name) $blockMap[$name] = $blockName;
|
|
}
|
|
}
|
|
node::$blockMap[$type] = $blockMap;
|
|
}
|
|
return node::$blockMap[$type];
|
|
}
|
|
|
|
public static function getNameFromBlockMap(string $type): ?string
|
|
{
|
|
$blockMap = static::getBlockMap();
|
|
if(str_starts_with($type, 'zin\\')) $type = substr($type, 4);
|
|
return isset($blockMap[$type]) ? $blockMap[$type] : null;
|
|
}
|
|
|
|
public static function definedPropsList(?string $type = null): array
|
|
{
|
|
if($type === null) $type = get_called_class();
|
|
|
|
if(!isset(node::$definedPropsMap[$type]) && $type === get_called_class())
|
|
{
|
|
node::$definedPropsMap[$type] = static::parsePropsDefinition();
|
|
}
|
|
return node::$definedPropsMap[$type];
|
|
}
|
|
|
|
public static function getDefaultProps(?string $type = null): array
|
|
{
|
|
$type = $type ? $type : get_called_class();
|
|
$defaultProps = array();
|
|
$definedPropsList = static::definedPropsList($type);
|
|
|
|
foreach($definedPropsList as $name => $definition)
|
|
{
|
|
if(!isset($definition['default'])) continue;
|
|
$defaultProps[$name] = $definition['default'];
|
|
}
|
|
return $defaultProps;
|
|
}
|
|
|
|
/**
|
|
* Parse props definition
|
|
* @param $definition
|
|
* @example
|
|
*
|
|
* $definition = array('name', 'desc:string', 'title?:string|array', 'icon?:string="star"');
|
|
* $definition = array('name' => 'mixed', 'desc' => '?string', 'title' => array('type' => 'string|array', 'optional' => true), 'icon' => array('type' => 'string', 'default' => 'star', 'optional' => true))))
|
|
*/
|
|
protected static function parsePropsDefinition(): array
|
|
{
|
|
$parentClass = get_parent_class(get_called_class());
|
|
$parentProps = array();
|
|
$defaultProps = static::$defaultProps;
|
|
$definition = static::$defineProps;
|
|
|
|
if($parentClass)
|
|
{
|
|
if($definition === $parentClass::$defineProps) $definition = array();
|
|
if($defaultProps === $parentClass::$defaultProps) $defaultProps = array();
|
|
|
|
$parentProps = call_user_func("$parentClass::definedPropsList", $parentClass);
|
|
}
|
|
|
|
return parsePropsMap($definition, $parentProps, $defaultProps);
|
|
}
|
|
}
|
|
|
|
function findInNode(array $selectors, node|array $list, bool $first = false, bool $reverse = false, bool $onlyContent = true): array
|
|
{
|
|
if($list instanceof node)
|
|
{
|
|
$data = $list->buildData;
|
|
if(!$data) return array();
|
|
|
|
$list = $data->content ? $data->content : array();
|
|
if(!$onlyContent) $list = array_merge($data->before ? $data->before : array(), $list, $data->after ? $data->after : array());
|
|
}
|
|
if($reverse) $list = array_reverse($list);
|
|
$result = array();
|
|
foreach($list as $child)
|
|
{
|
|
if(is_array($child))
|
|
{
|
|
$childList = findInNode($selectors, $child, $first, $reverse, $onlyContent);
|
|
if(!empty($childList))
|
|
{
|
|
if($first) return $childList;
|
|
$result = array_merge($result, $childList);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if(!($child instanceof node)) continue;
|
|
|
|
if($child->is($selectors) && $child->type() !== 'item')
|
|
{
|
|
if($child->parent && ($child->parent->removed || ($child->parent instanceof wg && $child->parent->type() !== 'item' && $child->parent->is($selectors)))) continue;
|
|
$result[$child->gid] = $child;
|
|
if($first) return $result;
|
|
}
|
|
|
|
$childList = findInNode($selectors, $child, $first, $reverse, $onlyContent);
|
|
if(!empty($childList))
|
|
{
|
|
if($first) return $childList;
|
|
$result = array_merge($result, $childList);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
function renderToHtml(mixed ...$items): string
|
|
{
|
|
$html = '';
|
|
|
|
foreach($items as $item)
|
|
{
|
|
if(is_array($item))
|
|
{
|
|
$html .= renderToHtml(...$item);
|
|
continue;
|
|
}
|
|
if($item instanceof node || (is_object($item) && method_exists($item, 'render')))
|
|
{
|
|
$html .= $item->render();
|
|
continue;
|
|
}
|
|
if(is_object($item) && isset($item->html))
|
|
{
|
|
$html .= $item->html;
|
|
continue;
|
|
}
|
|
if(!is_string($item)) $item = strval($item);
|
|
$html .= strval($item);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
function prebuild(array $items, ?node $parent = null)
|
|
{
|
|
foreach($items as $index => $item)
|
|
{
|
|
if(is_array($item))
|
|
{
|
|
$items[$index] = prebuild($item, $parent);
|
|
continue;
|
|
}
|
|
if(!($item instanceof node)) continue;
|
|
if($parent) $item->parent = $parent;
|
|
$item->prebuild();
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Parse the props definition.
|
|
*
|
|
* @param array $definition - The props definition.
|
|
* @param array $parentProps - The parent props.
|
|
* @param array $defaultValues - The default values.
|
|
* @return array
|
|
*/
|
|
function parsePropsMap(array $definition, array $parentProps = array(), array $defaultValues = array())
|
|
{
|
|
$props = $parentProps;
|
|
foreach($parentProps as $parentProp)
|
|
{
|
|
$name = $parentProp['name'];
|
|
if(isset($defaultValues[$name])) $parentProp['default'] = $defaultValues[$name];
|
|
$props[$name] = $parentProp;
|
|
}
|
|
|
|
foreach($definition as $name => $value)
|
|
{
|
|
$prop = parseProp($value, is_string($name) ? $name : null);
|
|
$name = $prop['name'];
|
|
|
|
if(isset($defaultValues[$name]))
|
|
{
|
|
$prop['default'] = $defaultValues[$name];
|
|
}
|
|
elseif(!isset($prop['default']) && isset($parentProps[$name]['default']) && $parentProps[$name]['default'])
|
|
{
|
|
$prop['default'] = $parentProps[$name]['default'];
|
|
}
|
|
|
|
$props[$name] = $prop;
|
|
}
|
|
|
|
return $props;
|
|
}
|
|
|
|
/**
|
|
* Parse the prop definition.
|
|
*
|
|
* @param string|array $definition - The prop definition.
|
|
* @param string|null $name - The prop name.
|
|
* @return array
|
|
*/
|
|
function parseProp(string|array $definition, ?string $name = null)
|
|
{
|
|
$optional = false;
|
|
$type = 'mixed';
|
|
$prop = array();
|
|
|
|
if(is_string($definition)) $definition = trim($definition);
|
|
|
|
/* Parse definition like `'name?: type1|type2="default"'` . */
|
|
if(!$name && is_string($definition))
|
|
{
|
|
if(str_contains($definition, ':'))
|
|
{
|
|
list($name, $definition) = explode(':', $definition, 2);
|
|
}
|
|
else
|
|
{
|
|
$name = $definition;
|
|
$definition = '';
|
|
}
|
|
$name = trim($name);
|
|
if(str_ends_with($name, '?'))
|
|
{
|
|
$name = substr($name, 0, strlen($name) - 1);
|
|
$optional = true;
|
|
}
|
|
}
|
|
|
|
/* Parse definition like `'name' => '?type1|type2="default"'` . */
|
|
if(is_array($definition))
|
|
{
|
|
if(isset($definition['type'])) $type = $definition['type'];
|
|
if(isset($definition['default'])) $prop['default'] = $definition['default'];
|
|
if(isset($definition['optional'])) $optional = $definition['optional'];
|
|
}
|
|
else if(is_string($definition))
|
|
{
|
|
if(str_contains($definition, '='))
|
|
{
|
|
list($type, $default) = explode('=', $definition, 2);
|
|
if(strlen($default)) $prop['default'] = json_decode(trim($default));
|
|
}
|
|
else
|
|
{
|
|
$type = $definition;
|
|
}
|
|
}
|
|
|
|
$type = trim($type);
|
|
if(str_starts_with($type, '?'))
|
|
{
|
|
$type = substr($type, 1);
|
|
$optional = true;
|
|
}
|
|
|
|
$typeList = explode('|', $type);
|
|
if(in_array('null', $typeList) || in_array('mixed', $typeList))
|
|
{
|
|
$optional = true;
|
|
}
|
|
elseif($optional)
|
|
{
|
|
array_unshift($typeList, 'null');
|
|
}
|
|
|
|
$prop['name'] = $name;
|
|
$prop['type'] = implode('|', $typeList);
|
|
$prop['optional'] = $optional || (isset($prop['default']) && $prop['default'] !== null);
|
|
return $prop;
|
|
}
|