578 lines
16 KiB
PHP
578 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
/**
|
|
* The context 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;
|
|
|
|
use function zin\utils\flat;
|
|
|
|
require_once dirname(__DIR__) . DS . 'utils' . DS . 'dataset.class.php';
|
|
require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php';
|
|
require_once dirname(__DIR__) . DS . 'utils' . DS . 'deep.func.php';
|
|
require_once __DIR__ . DS . 'helper.func.php';
|
|
require_once __DIR__ . DS . 'render.class.php';
|
|
require_once __DIR__ . DS . 'command.class.php';
|
|
require_once __DIR__ . DS . 'js.class.php';
|
|
|
|
class context extends \zin\utils\dataset
|
|
{
|
|
public string $name;
|
|
|
|
public array $globalRenderList = array();
|
|
|
|
public int $globalRenderLevel = 0;
|
|
|
|
public array $data = array();
|
|
|
|
public bool $rendered = false;
|
|
|
|
public bool $rawContentCalled = false;
|
|
|
|
public bool $hookContentCalled = false;
|
|
|
|
public array $beforeBuildNodeCallbacks = array();
|
|
|
|
public array $onBuildNodeCallbacks = array();
|
|
|
|
public array $onRenderNodeCallbacks = array();
|
|
|
|
public array $onRenderCallbacks = array();
|
|
|
|
public array $queries = array();
|
|
|
|
public ?node $rootNode = null;
|
|
|
|
public ?render $renderer = null;
|
|
|
|
public array $pageJS = array();
|
|
|
|
public array $pageCSS = array();
|
|
|
|
public array $jsVars = array();
|
|
|
|
public array $jsCalls = array();
|
|
|
|
public array $wgRes = array();
|
|
|
|
public array $eventBindings = array();
|
|
|
|
public array $renderWgMap = array('page' => 'page', 'modal' => 'modalDialog', 'fragment' => 'fragment');
|
|
|
|
public function __construct(string $name)
|
|
{
|
|
parent::__construct();
|
|
$this->name = $name;
|
|
}
|
|
|
|
public function __debugInfo(): array
|
|
{
|
|
return array_merge(array
|
|
(
|
|
'name' => $this->name,
|
|
'globalRenderListLen' => count($this->globalRenderList),
|
|
'globalRenderList' => $this->globalRenderList,
|
|
'globalRenderLevel' => $this->globalRenderLevel,
|
|
'rendered' => $this->rendered,
|
|
'rawContentCalled' => $this->rawContentCalled
|
|
), $this->storedData);
|
|
}
|
|
|
|
public function getData(string $namePath, mixed $defaultValue = null): mixed
|
|
{
|
|
return \zin\utils\deepGet($this->data, $namePath, $defaultValue);
|
|
}
|
|
|
|
public function setData(string $namePath, mixed $value)
|
|
{
|
|
\zin\utils\deepSet($this->data, $namePath, $value);
|
|
}
|
|
|
|
public function enableGlobalRender()
|
|
{
|
|
$this->globalRenderLevel--;
|
|
}
|
|
|
|
public function disableGlobalRender()
|
|
{
|
|
$this->globalRenderLevel++;
|
|
}
|
|
|
|
public function enabledGlobalRender()
|
|
{
|
|
return $this->globalRenderLevel < 1;
|
|
}
|
|
|
|
public function renderInGlobal(node|iDirective $item): bool
|
|
{
|
|
if($this->globalRenderLevel > 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if($item instanceof node)
|
|
{
|
|
if($item->parent) return false;
|
|
|
|
$type = $item->type();
|
|
if($type === 'wg' || $type === 'node') return false;
|
|
|
|
if(!isset($this->globalRenderList[$item->gid])) $this->globalRenderList[$item->gid] = $item;
|
|
return true;
|
|
}
|
|
|
|
if(in_array($item, $this->globalRenderList)) return false;
|
|
|
|
$this->globalRenderList[] = $item;
|
|
return true;
|
|
}
|
|
|
|
function skipRenderInGlobal(mixed $data)
|
|
{
|
|
if(is_array($data))
|
|
{
|
|
foreach($data as $item) skipRenderInGlobal($item);
|
|
return;
|
|
}
|
|
|
|
if($data instanceof node || $data instanceof iDirective)
|
|
{
|
|
if(isset($data->gid)) unset($this->globalRenderList[$data->gid]);
|
|
$data->notRenderInGlobal = true;
|
|
}
|
|
}
|
|
|
|
public function getGlobalRenderList(bool $clear = true): array
|
|
{
|
|
$globalItems = array();
|
|
|
|
foreach($this->globalRenderList as $item)
|
|
{
|
|
if(is_object($item) && ((isset($item->parent) && $item->parent) || (isset($item->notRenderInGlobal) && $item->notRenderInGlobal)))
|
|
{
|
|
continue;
|
|
}
|
|
$globalItems[] = $item;
|
|
}
|
|
|
|
/* Clear globalRenderList. */
|
|
if($clear) $this->globalRenderList = array();
|
|
|
|
return $globalItems;
|
|
}
|
|
|
|
public function addHookFiles(string|array ...$files)
|
|
{
|
|
$files = flat($files);
|
|
return $this->mergeToList('hookFiles', array_filter(array_values($files)));
|
|
}
|
|
|
|
public function getHookFiles(): array
|
|
{
|
|
return $this->getList('hookFiles');
|
|
}
|
|
|
|
public function addImports(string ...$files)
|
|
{
|
|
return $this->mergeToList('import', $files);
|
|
}
|
|
|
|
public function getImports(): array
|
|
{
|
|
return $this->getList('import');
|
|
}
|
|
|
|
public function addCSS(string|array $css, ?string $name = null)
|
|
{
|
|
if(is_array($css)) $css = implode("\n", $css);
|
|
|
|
if($name)
|
|
{
|
|
if(isset($this->pageCSS[$name]))
|
|
{
|
|
if(isDebug()) triggerError("Page CSS name \"$name\" already exists.");
|
|
return;
|
|
}
|
|
$this->pageCSS[$name] = $css;
|
|
}
|
|
else
|
|
{
|
|
$this->pageCSS[] = $css;
|
|
}
|
|
}
|
|
|
|
public function getCSS(): string
|
|
{
|
|
$css = array();
|
|
$wgRes = $this->wgRes;
|
|
|
|
if($wgRes) foreach ($wgRes as $res) if($res['css']) $css[] = $res['css'];
|
|
|
|
if($this->pageCSS) $css = array_merge($css, $this->pageCSS);
|
|
return trim(implode("\n", $css));
|
|
}
|
|
|
|
public function addJS(string|array $js, ?string $name = null)
|
|
{
|
|
if(is_array($js)) $js = implode("\n", $js);
|
|
|
|
if($name)
|
|
{
|
|
if(isset($this->pageJS[$name]))
|
|
{
|
|
if(isDebug()) triggerError("Page JS name \"$name\" already exists.");
|
|
return;
|
|
}
|
|
$this->pageJS[$name] = $js;
|
|
}
|
|
else
|
|
{
|
|
$this->pageJS[] = $js;
|
|
}
|
|
}
|
|
|
|
public function addJSVar(string $name, mixed $value)
|
|
{
|
|
$this->jsVars[$name] = $value;
|
|
}
|
|
|
|
public function addJSCall($func, $args)
|
|
{
|
|
$this->jsCalls[] = array($func, $args);
|
|
}
|
|
|
|
public function getJS()
|
|
{
|
|
$jsVars = $this->jsVars;
|
|
$pageJS = $this->pageJS;
|
|
$jsCalls = $this->jsCalls;
|
|
$wgRes = $this->wgRes;
|
|
$eventBindings = $this->eventBindings;
|
|
$js = array();
|
|
|
|
if($wgRes) foreach ($wgRes as $res) if($res['js']) $js[] = js::scope($res['js']);
|
|
if($jsVars) foreach($jsVars as $name => $value) $js[] = js::defineVar($name, $value);
|
|
if($pageJS) $js = array_merge($js, $pageJS);
|
|
if($eventBindings) $js = array_merge($js, $eventBindings);
|
|
if($jsCalls) foreach($jsCalls as $call) $js[] = js::defineJSCall($call[0], $call[1]);
|
|
|
|
if(empty($js)) return '';
|
|
|
|
$js = trim(implode("\n", $js));
|
|
if(strpos($js, 'setTimeout') !== false) $js = 'function setTimeout(callback, time){return typeof window.registerTimer === "function" ? window.registerTimer(callback, time) : window.setTimeout(callback, time);}' . $js;
|
|
if(strpos($js, 'setInterval') !== false) $js = 'function setInterval(callback, time){return typeof window.registerTimer === "function" ? window.registerTimer(callback, time, "interval") : window.setInterval(callback, time);}' . $js;
|
|
|
|
$methods = array('onPageUnmount', 'beforePageUpdate', 'afterPageUpdate', 'onPageRender');
|
|
foreach($methods as $method)
|
|
{
|
|
if(strpos($js, $method) === false) continue;
|
|
$js .= "if(typeof $method === 'function') window.$method = $method;";
|
|
}
|
|
return $js;
|
|
}
|
|
|
|
public function getDebugData() : ?array
|
|
{
|
|
global $app;
|
|
$zinDebug = null;
|
|
if(isDebug() && (!isAjaxRequest() || isAjaxRequest('zin')))
|
|
{
|
|
$zinDebug = data('zinDebug');
|
|
if(is_array($zinDebug))
|
|
{
|
|
$zinDebug['basePath'] = $app->getBasePath();
|
|
if(isset($app->zinErrors)) $zinDebug['errors'] = $app->zinErrors;
|
|
}
|
|
}
|
|
return $zinDebug;
|
|
}
|
|
|
|
public function getRawContent(): string
|
|
{
|
|
$rawContent = ob_get_contents();
|
|
if(!is_string($rawContent)) $rawContent = '';
|
|
ob_end_clean();
|
|
return $rawContent;
|
|
}
|
|
|
|
public function addQuery(query $query)
|
|
{
|
|
$this->queries[] = $query;
|
|
}
|
|
|
|
/**
|
|
* Include hooks files.
|
|
*/
|
|
public function includeHooks(): string
|
|
{
|
|
$hookFiles = $this->getHookFiles();
|
|
ob_start();
|
|
foreach($hookFiles as $hookFile)
|
|
{
|
|
if(!empty($hookFile) && file_exists($hookFile)) include $hookFile;
|
|
}
|
|
$hookCode = ob_get_clean();
|
|
ob_end_flush();
|
|
if($hookCode) return $hookCode;
|
|
return '';
|
|
}
|
|
|
|
public function render(node $node, null|object|string|array $selectors = null, string $renderType = 'html', bool $renderInner = false): string|array|object
|
|
{
|
|
$this->disableGlobalRender();
|
|
|
|
$renderer = new render($node, $selectors, $renderType, $renderInner);
|
|
$this->rendered = true;
|
|
$this->renderer = $renderer;
|
|
$this->rootNode = $node;
|
|
|
|
$hookCode = $this->includeHooks();
|
|
$rawContent = $this->getRawContent();
|
|
$zinDebug = $this->getDebugData();
|
|
|
|
$node->prebuild(true);
|
|
$this->applyQueries($node);
|
|
|
|
$js = $this->getJS();
|
|
$css = $this->getCSS();
|
|
$result = $renderer->render();
|
|
|
|
if(is_object($result)) // renderType = json
|
|
{
|
|
if($zinDebug && isset($result->zinDebug)) $result->zinDebug = $zinDebug;
|
|
$result = json_encode($result, JSON_PARTIAL_OUTPUT_ON_ERROR);
|
|
}
|
|
elseif(is_array($result)) // renderType = list
|
|
{
|
|
foreach($result as $item)
|
|
{
|
|
if($item->name === 'zinDebug' && $zinDebug)
|
|
{
|
|
$item->data = $zinDebug;
|
|
continue;
|
|
}
|
|
if($item->name === 'hookCode')
|
|
{
|
|
$item->data = $hookCode;
|
|
continue;
|
|
}
|
|
if(!isset($item->type) || $item->type !== 'html') continue;
|
|
|
|
$replace = array('<!-- {{RAW_CONTENT}} -->' => $rawContent, '<!-- {{HOOK_CONTENT}} -->' => $hookCode, '/*{{ZIN_PAGE_CSS}}*/' => $css, '/*{{ZIN_PAGE_JS}}*/' => $js);
|
|
$item->data = str_replace(array_keys($replace), array_values($replace), $item->data);
|
|
}
|
|
|
|
$result = json_encode($result, JSON_PARTIAL_OUTPUT_ON_ERROR);
|
|
}
|
|
else // renderType = html
|
|
{
|
|
if($zinDebug) $js .= js::defineVar('window.zinDebug', $zinDebug);
|
|
|
|
$replace = array('<!-- {{RAW_CONTENT}} -->' => $rawContent, '<!-- {{HOOK_CONTENT}} -->' => $hookCode, '/*{{ZIN_PAGE_CSS}}*/' => $css, '/*{{ZIN_PAGE_JS}}*/' => $js);
|
|
$result = str_replace(array_keys($replace), array_values($replace), $result);
|
|
}
|
|
|
|
$data = new stdClass();
|
|
$data->type = $renderType;
|
|
$data->output = $result;
|
|
|
|
foreach($this->onRenderCallbacks as $callback)
|
|
{
|
|
if($callback instanceof \Closure) $callback($data, $node);
|
|
else call_user_func($callback, $data, $node);
|
|
}
|
|
|
|
$this->enabledGlobalRender();
|
|
return $data->output;
|
|
}
|
|
|
|
protected function applyQueries(node $rootNode)
|
|
{
|
|
if(!$this->queries) return;
|
|
|
|
foreach($this->queries as $query)
|
|
{
|
|
$this->applyQuery($rootNode, $query);
|
|
}
|
|
}
|
|
|
|
protected function applyQuery(node $rootNode, query $query)
|
|
{
|
|
$nodes = $query->isRoot() ? array($rootNode) : findInNode($query->selectors, $rootNode, false, false, false);
|
|
if(!$nodes) return;
|
|
|
|
$queryNodes = $nodes;
|
|
foreach($query->commands as $command)
|
|
{
|
|
list($method, $args) = $command;
|
|
$result = call_user_func("\zin\command::{$method}", $queryNodes, ...$args);
|
|
if(is_array($result)) $queryNodes = $result;
|
|
if(empty($queryNodes)) break;
|
|
}
|
|
}
|
|
|
|
public function handleBeforeBuildNode(node $node)
|
|
{
|
|
foreach($this->beforeBuildNodeCallbacks as $callback)
|
|
{
|
|
if($callback instanceof \Closure) $callback($node);
|
|
else call_user_func($callback, $node);
|
|
}
|
|
}
|
|
|
|
public function handleBuildNode(stdClass &$data, node $node)
|
|
{
|
|
foreach($this->onBuildNodeCallbacks as $callback)
|
|
{
|
|
if($callback instanceof \Closure) $callback($data, $node);
|
|
else call_user_func($callback, $data, $node);
|
|
}
|
|
|
|
if($node instanceof wg)
|
|
{
|
|
$class = get_class($node);
|
|
if(!isset($this->wgRes[$class]))
|
|
{
|
|
$res = array();
|
|
$res['css'] = $class::getPageCSS();
|
|
$res['js'] = $class::getPageJS();
|
|
$this->wgRes[$class] = $res;
|
|
}
|
|
}
|
|
|
|
if($this->renderer) $this->renderer->handleBuildNode($data, $node);
|
|
|
|
$eventBinding = $node->buildEvents();
|
|
if($eventBinding) $this->eventBindings[] = $eventBinding;
|
|
}
|
|
|
|
public function handleRenderNode(stdClass &$data, node $node)
|
|
{
|
|
foreach($this->onRenderNodeCallbacks as $callback)
|
|
{
|
|
if($callback instanceof \Closure) $callback($data, $node);
|
|
else call_user_func($callback, $data, $node);
|
|
}
|
|
}
|
|
|
|
public function onBuildNode(callable|\Closure $callback)
|
|
{
|
|
$this->onBuildNodeCallbacks[] = $callback;
|
|
}
|
|
|
|
public function onRenderNode(callable|\Closure $callback)
|
|
{
|
|
$this->onRenderNodeCallbacks[] = $callback;
|
|
}
|
|
|
|
public function onBeforeBuildNode(callable|\Closure $callback)
|
|
{
|
|
$this->beforeBuildNodeCallbacks[] = $callback;
|
|
}
|
|
|
|
public function onRender(callable|\Closure $callback)
|
|
{
|
|
$this->onRenderCallbacks[] = $callback;
|
|
}
|
|
|
|
public function setRenderWgMap(string|array $mapOrName, ?string $wgName)
|
|
{
|
|
if(is_array($mapOrName))
|
|
{
|
|
$this->renderWgMap = array_merge($this->renderWgMap, $mapOrName);
|
|
}
|
|
else
|
|
{
|
|
$this->renderWgMap[$mapOrName] = $wgName;
|
|
}
|
|
}
|
|
|
|
public function getRenderWgName()
|
|
{
|
|
if(isset($this->renderWgMap['all'])) return $this->renderWgMap['all'];
|
|
if(isAjaxRequest('modal')) return $this->renderWgMap['modal'];
|
|
if(isAjaxRequest() && !isAjaxRequest('zin')) return $this->renderWgMap['fragment'];
|
|
return 'page';
|
|
}
|
|
|
|
public static array $stack = array();
|
|
|
|
public static function js(string ...$code)
|
|
{
|
|
static::current()->addJS(flat($code));
|
|
}
|
|
|
|
public static function jsCall(string $func, mixed ...$args)
|
|
{
|
|
static::current()->addJSCall($func, $args);
|
|
}
|
|
|
|
public static function jsVar($name, $value)
|
|
{
|
|
static::current()->addJSVar($name, $value);
|
|
}
|
|
|
|
public static function css(string ...$code)
|
|
{
|
|
static::current()->addCSS(flat($code));
|
|
}
|
|
|
|
public static function import(string ...$files)
|
|
{
|
|
static::current()->addImports(...$files);
|
|
}
|
|
|
|
/**
|
|
* Get current context.
|
|
*
|
|
* @access public
|
|
* @return context
|
|
*/
|
|
public static function current(): context
|
|
{
|
|
if(empty(static::$stack))
|
|
{
|
|
$context = new context('default');
|
|
static::$stack['default'] = $context;
|
|
return $context;
|
|
}
|
|
return end(static::$stack);
|
|
}
|
|
|
|
/**
|
|
* Create context.
|
|
*
|
|
* @access public
|
|
* @param string $name Context name.
|
|
* @return context
|
|
*/
|
|
public static function create(string $name): context
|
|
{
|
|
if(isset(static::$stack[$name]))
|
|
{
|
|
triggerError("Context name \"$name\" already exists.");
|
|
}
|
|
$context = new context($name);
|
|
static::$stack[$name] = $context;
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* Pop last context.
|
|
*
|
|
* @access public
|
|
* @return ?context
|
|
*/
|
|
public static function pop(): ?context
|
|
{
|
|
return array_pop(static::$stack);
|
|
}
|
|
}
|