+ add zin and zui3 files.
This commit is contained in:
@@ -394,19 +394,20 @@ class baseControl
|
||||
*
|
||||
* @param string $moduleName module name
|
||||
* @param string $methodName method name
|
||||
* @param string $viewDir
|
||||
* @access public
|
||||
* @return string the view file
|
||||
*/
|
||||
public function setViewFile($moduleName, $methodName)
|
||||
public function setViewFile(string $moduleName, string $methodName, string $viewDir = 'view')
|
||||
{
|
||||
$moduleName = strtolower(trim($moduleName));
|
||||
$methodName = strtolower(trim($methodName));
|
||||
|
||||
$modulePath = $this->app->getModulePath($this->appName, $moduleName);
|
||||
$viewExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'view');
|
||||
$viewExtPath = $this->app->getModuleExtPath($moduleName, $viewDir);
|
||||
|
||||
$viewType = $this->viewType == 'mhtml' ? 'html' : $this->viewType;
|
||||
$mainViewFile = $modulePath . 'view' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php';
|
||||
$mainViewFile = $modulePath . $viewDir . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php';
|
||||
$viewFile = $mainViewFile;
|
||||
|
||||
if(!empty($viewExtPath))
|
||||
@@ -416,7 +417,7 @@ class baseControl
|
||||
|
||||
$viewFile = file_exists($commonExtViewFile) ? $commonExtViewFile : $mainViewFile;
|
||||
$viewFile = (!empty($siteExtViewFile) and file_exists($siteExtViewFile)) ? $siteExtViewFile : $viewFile;
|
||||
if(!is_file($viewFile)) $this->app->triggerError("the view file $viewFile not found", __FILE__, __LINE__, $exit = true);
|
||||
if(!is_file($viewFile)) $this->app->triggerError("the view file $viewFile not found", __FILE__, __LINE__, true);
|
||||
|
||||
$commonExtHookFiles = glob($viewExtPath['common'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
|
||||
$siteExtHookFiles = empty($viewExtPath['site']) ? '' : glob($viewExtPath['site'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
|
||||
@@ -907,12 +908,106 @@ class baseControl
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function display($moduleName = '', $methodName = '')
|
||||
public function display(string $moduleName = '', string $methodName = '')
|
||||
{
|
||||
if($this->config->debug && $this->viewType === 'html' && (!isset($_GET['zin']) || $_GET['zin'] != '0'))
|
||||
{
|
||||
if(empty($moduleName)) $moduleName = $this->moduleName;
|
||||
if(empty($methodName)) $methodName = $this->methodName;
|
||||
$modulePath = $this->app->getModulePath($this->appName, $moduleName);
|
||||
$viewType = $this->viewType == 'mhtml' ? 'html' : $this->viewType;
|
||||
$mainViewFile = $modulePath . 'ui' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php';
|
||||
if(file_exists($mainViewFile)) return $this->render($moduleName, $methodName);
|
||||
}
|
||||
|
||||
if(empty($this->output)) $this->parse($moduleName, $methodName);
|
||||
|
||||
echo $this->output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向浏览器输出内容。
|
||||
* Print the content of the view.
|
||||
*
|
||||
* @param string $moduleName module name
|
||||
* @param string $methodName method name
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function render($moduleName = '', $methodName = '')
|
||||
{
|
||||
if(isset($_GET['zin']) && $_GET['zin'] == '0')
|
||||
{
|
||||
$this->display($moduleName, $methodName);
|
||||
return;
|
||||
}
|
||||
|
||||
if(empty($moduleName)) $moduleName = $this->moduleName;
|
||||
if(empty($methodName)) $methodName = $this->methodName;
|
||||
|
||||
/* Load zin lib */
|
||||
$this->app->loadClass('zin', true);
|
||||
\zin\loadConfig();
|
||||
|
||||
/**
|
||||
* 设置视图文件。(PHP7有一个bug,不能直接$viewFile = $this->setViewFile())。
|
||||
* Set viewFile. (Can't assign $viewFile = $this->setViewFile() directly because one php7's bug.)
|
||||
*/
|
||||
$results = $this->setViewFile($moduleName, $methodName, 'ui');
|
||||
|
||||
$viewFile = $results;
|
||||
if(is_array($results)) extract($results);
|
||||
|
||||
/**
|
||||
* 获得当前页面的CSS和JS。
|
||||
* Get css and js codes for current method.
|
||||
*/
|
||||
$css = $this->getCSS($moduleName, $methodName, '.ui');
|
||||
$js = $this->getJS($moduleName, $methodName, '.ui');
|
||||
if($css) $this->view->pageCSS = $css;
|
||||
if($js) $this->view->pageJS = $js;
|
||||
|
||||
/**
|
||||
* 切换到视图文件所在的目录,以保证视图文件里面的include语句能够正常运行。
|
||||
* Change the dir to the view file to keep the relative paths work.
|
||||
*/
|
||||
$currentPWD = getcwd();
|
||||
chdir(dirname($viewFile));
|
||||
|
||||
/**
|
||||
* Set zin context data
|
||||
*/
|
||||
\zin\zin::$globalRenderList = array();
|
||||
\zin\zin::$enabledGlobalRender = true;
|
||||
\zin\zin::$rendered = false;
|
||||
\zin\zin::$rawContentCalled = false;
|
||||
|
||||
\zin\zin::$data = (array)$this->view;
|
||||
\zin\zin::$data['zinDebug'] = array();
|
||||
if($this->config->debug && $this->config->debug >= 2)
|
||||
{
|
||||
\zin\zin::$data['zinDebug']['trace'] = $this->app->loadClass('trace')->getTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用extract和ob方法渲染$viewFile里面的代码。
|
||||
* Use extract and ob functions to eval the codes in $viewFile.
|
||||
*/
|
||||
extract(\zin\zin::$data);
|
||||
ob_start();
|
||||
include $viewFile;
|
||||
if(!\zin\zin::$rendered) \zin\render();
|
||||
$content = ob_get_clean();
|
||||
ob_start();
|
||||
echo $content;
|
||||
|
||||
/**
|
||||
* 渲染完毕后,再切换回之前的路径。
|
||||
* At the end, chang the dir to the previous.
|
||||
*/
|
||||
chdir($currentPWD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接输出data数据,通常用于ajax请求中。
|
||||
* Send data directly, for ajax requests.
|
||||
|
||||
@@ -680,15 +680,21 @@ class baseHelper
|
||||
* 检查是否是AJAX请求。
|
||||
* Check is ajax request.
|
||||
*
|
||||
* @param ?string $type zin|modal|fetch
|
||||
* @static
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAjaxRequest()
|
||||
public static function isAjaxRequest(?string $type = null): bool
|
||||
{
|
||||
if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') return true;
|
||||
if(isset($_GET['HTTP_X_REQUESTED_WITH']) && $_GET['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') return true;
|
||||
return false;
|
||||
$isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') || (isset($_GET['HTTP_X_REQUESTED_WITH']) && $_GET['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
|
||||
if($isAjax === false) return false;
|
||||
|
||||
if($type === 'zin') return array_key_exists('HTTP_X_ZIN_OPTIONS', $_SERVER);
|
||||
if($type === 'modal') return isset($_SERVER['HTTP_X_ZUI_MODAL']) && $_SERVER['HTTP_X_ZUI_MODAL'] == true;
|
||||
if($type === 'fetch') return !array_key_exists('HTTP_X_ZIN_OPTIONS', $_SERVER) && !(isset($_SERVER['HTTP_X_ZUI_MODAL']) && $_SERVER['HTTP_X_ZUI_MODAL'] == true);
|
||||
|
||||
return $isAjax;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -788,6 +794,101 @@ class baseHelper
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a cookie.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param int|null $expire
|
||||
* @param string|null $path
|
||||
* @param string $domain
|
||||
* @param bool|null $secure
|
||||
* @param bool $httponly
|
||||
* @static
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public static function setcookie(string $name, string $value = '', int $expire = null, string $path = null, string $domain = '', bool $secure = null, bool $httponly = true)
|
||||
{
|
||||
global $config, $app;
|
||||
|
||||
if($expire === null) $expire = $config->cookieLife;
|
||||
if($path === null) $path = $config->webRoot;
|
||||
if($secure === null) $secure = $config->cookieSecure;
|
||||
|
||||
if(isset($app->worker))
|
||||
{
|
||||
$app->worker->response->setCookie($name, $value, $expire, $path, $domain, $secure, $httponly);
|
||||
}
|
||||
else
|
||||
{
|
||||
return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态码。
|
||||
* Set status code.
|
||||
*
|
||||
* @param int $code
|
||||
* @static
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
static public function setStatus(int $code)
|
||||
{
|
||||
global $app;
|
||||
|
||||
if(isset($app->worker))
|
||||
{
|
||||
$app->worker->response->setStatus($code);
|
||||
}
|
||||
else
|
||||
{
|
||||
$PHRASES = array(
|
||||
100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
|
||||
200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
|
||||
300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
|
||||
400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
|
||||
500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
|
||||
);
|
||||
header('HTTP/1.1 ' . (string)$code . ' ' . $PHRASES[$code], true, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP头信息。
|
||||
* Send http header.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @param bool $replace
|
||||
* @param int $response_code
|
||||
* @static
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
static public function header(string $key, string $value, bool $replace = true, int $response_code = 0)
|
||||
{
|
||||
global $app;
|
||||
|
||||
if(isset($app->worker))
|
||||
{
|
||||
$key = trim(strtolower($key));
|
||||
$app->worker->response->setHeader($key, $value);
|
||||
|
||||
if($key == 'location')
|
||||
{
|
||||
$app->worker->response->setStatus(302);
|
||||
helper::end();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
header($key . ': ' . $value, $replace, $response_code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用缓存。
|
||||
* Check is enable cache.
|
||||
|
||||
@@ -356,6 +356,14 @@ class baseRouter
|
||||
*/
|
||||
public $siteCode;
|
||||
|
||||
/**
|
||||
* zin 请求时发生的错误信息。
|
||||
* The errors occurred when zin request.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $zinErrors = array();
|
||||
|
||||
/**
|
||||
* 构造方法, 设置路径,类,超级变量等。注意:
|
||||
* 1.应该使用createApp()方法实例化router类;
|
||||
@@ -2891,7 +2899,7 @@ class baseRouter
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function saveError($level, $message, $file, $line)
|
||||
public function saveError(int $level, string $message, string $file, int $line)
|
||||
{
|
||||
if(empty($this->config->debug)) return true;
|
||||
if(!is_dir($this->logRoot)) return true;
|
||||
@@ -2901,9 +2909,9 @@ class baseRouter
|
||||
* 删除设定时间之前的日志。
|
||||
* Delete the log before the set time.
|
||||
**/
|
||||
if(mt_rand(0, 10) == 1)
|
||||
if(random_int(0, 10) == 1)
|
||||
{
|
||||
$logDays = isset($this->config->framework->logDays) ? $this->config->framework->logDays : 14;
|
||||
$logDays = $this->config->framework->logDays ?? 14;
|
||||
$dayTime = time() - $logDays * 24 * 3600;
|
||||
foreach(glob($this->getLogRoot() . '*') as $logFile)
|
||||
{
|
||||
@@ -2915,7 +2923,7 @@ class baseRouter
|
||||
* 忽略该错误:Redefining already defined constructor。
|
||||
* Skip the error: Redefining already defined constructor.
|
||||
**/
|
||||
if(strpos($message, 'Redefining') !== false) return true;
|
||||
if(str_contains($message, 'Redefining')) return true;
|
||||
|
||||
/*
|
||||
* 设置错误信息。
|
||||
@@ -2924,8 +2932,8 @@ class baseRouter
|
||||
if(preg_match('/[^\x00-\x80]/', $message)) $message = helper::convertEncoding($message, 'gbk');
|
||||
$errorLog = "\n" . date('H:i:s') . " $message in <strong>$file</strong> on line <strong>$line</strong> ";
|
||||
|
||||
$URI = $this->getURI();
|
||||
$errorLog .= "when visiting <strong>" . (empty($URI) ? '' : htmlspecialchars($URI)) . "</strong>\n";
|
||||
$uri = $this->getURI();
|
||||
$errorLog .= "when visiting <strong>" . (empty($uri) ? '' : htmlspecialchars($uri)) . "</strong>\n";
|
||||
|
||||
/*
|
||||
* 为了安全起见,对公网环境隐藏脚本路径。
|
||||
@@ -2942,18 +2950,29 @@ class baseRouter
|
||||
if(!is_file($errorFile)) file_put_contents($errorFile, "<?php\n die();\n?" . ">\n");
|
||||
|
||||
$fh = fopen($errorFile, 'a');
|
||||
if($fh) fwrite($fh, strip_tags($errorLog)) and fclose($fh);
|
||||
if($fh) fwrite($fh, strip_tags(htmlspecialchars_decode($errorLog))) and fclose($fh);
|
||||
|
||||
/*
|
||||
* 如果debug > 1,显示warning, notice级别的错误。
|
||||
* If the debug > 1, show warning, notice error.
|
||||
* 如果debug > 1,直接在页面显示非严重错误。
|
||||
* If the debug > 1, show non-serious errors on page directly.
|
||||
**/
|
||||
if($level == E_NOTICE or $level == E_WARNING or $level == E_STRICT or $level == 8192) // 8192: E_DEPRECATED
|
||||
if(!empty($this->config->debug) && $this->config->debug > 1)
|
||||
{
|
||||
if(!empty($this->config->debug) and $this->config->debug > 1)
|
||||
/* Send non-serious errors to page in zin mode. */
|
||||
$isZinRequest = isset($this->config->zin) || isset($_SERVER['HTTP_X_ZIN_OPTIONS']);
|
||||
$isNonSeriousError = $level !== E_ERROR && $level !== E_PARSE && $level !== E_CORE_ERROR && $level !== E_COMPILE_ERROR;
|
||||
if($isZinRequest && $isNonSeriousError)
|
||||
{
|
||||
$this->zinErrors[] = array('file' => $file, 'line' => $line, 'message' => $message, 'level' => $level);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Show non-serious errors to classic page. */
|
||||
if($level == E_NOTICE or $level == E_WARNING or $level == E_STRICT or $level == 8192)
|
||||
{
|
||||
$cmd = "vim +$line $file";
|
||||
$size = strlen($cmd);
|
||||
|
||||
echo "<pre class='alert alert-danger'>$message: ";
|
||||
echo "<input type='text' value='$cmd' size='$size' style='border:none; background:none;' onclick='this.select();' /></pre>";
|
||||
}
|
||||
@@ -2963,14 +2982,21 @@ class baseRouter
|
||||
* 如果是严重错误,停止程序。
|
||||
* If error level is serious, die.
|
||||
* */
|
||||
if($level == E_ERROR or $level == E_PARSE or $level == E_CORE_ERROR or $level == E_COMPILE_ERROR or $level == E_USER_ERROR)
|
||||
if(in_array($level, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR)))
|
||||
{
|
||||
if(empty($this->config->debug)) die();
|
||||
if(PHP_SAPI == 'cli') die($errorLog);
|
||||
if(empty($this->config->debug)) helper::end();
|
||||
|
||||
$htmlError = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8' /></head>";
|
||||
$htmlError .= "<body>" . nl2br($errorLog) . "</body></html>";
|
||||
die($htmlError);
|
||||
if(PHP_SAPI == 'cli')
|
||||
{
|
||||
echo $errorLog;
|
||||
}
|
||||
else
|
||||
{
|
||||
$htmlError = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8' /></head>";
|
||||
$htmlError .= "<body>" . nl2br($errorLog) . "</body></html>";
|
||||
echo $htmlError;
|
||||
helper::end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,19 +249,20 @@ class control extends baseControl
|
||||
*
|
||||
* @param string $moduleName module name
|
||||
* @param string $methodName method name
|
||||
* @param string $viewDir
|
||||
* @access public
|
||||
* @return string the view file
|
||||
*/
|
||||
public function setViewFile($moduleName, $methodName)
|
||||
public function setViewFile(string $moduleName, string $methodName, string $viewDir = 'view')
|
||||
{
|
||||
$moduleName = strtolower(trim($moduleName));
|
||||
$methodName = strtolower(trim($methodName));
|
||||
|
||||
$modulePath = $this->app->getModulePath($this->appName, $moduleName);
|
||||
$viewExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'view');
|
||||
$viewExtPath = $this->app->getModuleExtPath($moduleName, $viewDir, 'view');
|
||||
|
||||
$viewType = ($this->viewType == 'mhtml' or $this->viewType == 'xhtml') ? 'html' : $this->viewType;
|
||||
$mainViewFile = $modulePath . 'view' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php';
|
||||
$mainViewFile = $modulePath . $viewDir . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php';
|
||||
|
||||
/* If the main view file doesn't exist, set the device prefix to empty and reset the main view file. */
|
||||
if(!file_exists($mainViewFile) and $this->app->clientDevice != 'mobile')
|
||||
@@ -309,8 +310,8 @@ class control extends baseControl
|
||||
$viewFile = $commonExtViewFile;
|
||||
}
|
||||
|
||||
if(!is_file($viewFile)) $viewFile = dirname(dirname($viewExtPath['common'])) . DS . 'view' . DS . $this->devicePrefix . $methodName . ".{$viewType}.php";
|
||||
if(!is_file($viewFile)) die(js::error($this->lang->notPage) . js::locate('back'));
|
||||
if(!is_file($viewFile)) $viewFile = dirname((string) $viewExtPath['common'], 2) . DS . 'view' . DS . $this->devicePrefix . $methodName . ".{$viewType}.php";
|
||||
if(!is_file($viewFile)) helper::end(js::error($this->lang->notPage) . js::locate('back'));
|
||||
|
||||
/* Get ext hook files. */
|
||||
$commonExtHookFiles = glob($viewExtPath['common'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
|
||||
|
||||
@@ -373,6 +373,18 @@ function isonlybody()
|
||||
return helper::inOnlyBodyMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页面是否是弹窗中。
|
||||
* Check page is modal.
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
function isInModal(): bool
|
||||
{
|
||||
return helper::isAjaxRequest('modal');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time.
|
||||
*
|
||||
@@ -406,3 +418,251 @@ function autoloader($class)
|
||||
}
|
||||
|
||||
spl_autoload_register('autoloader');
|
||||
|
||||
|
||||
/**
|
||||
* Init page title based on the module name and the method name.
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function initPageTitle(): string
|
||||
{
|
||||
global $app, $lang;
|
||||
$module = $app->rawModule;
|
||||
$method = $app->rawMethod;
|
||||
|
||||
if(empty($lang->$module)) $app->loadLang($module);
|
||||
|
||||
if(!empty($lang->$module->{$method . 'Action'})) return $lang->$module->{$method . 'Action'};
|
||||
if(!empty($lang->$module->$method)) return $lang->$module->$method;
|
||||
return zget($lang, $method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init page entity based on configuration of objectNameFields.
|
||||
*
|
||||
* @param object $object
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function initPageEntity(object $object): array
|
||||
{
|
||||
if(empty($object)) return array();
|
||||
|
||||
global $app, $config;
|
||||
$app->loadModuleConfig('action');
|
||||
|
||||
$module = $app->getModuleName();
|
||||
$idField = isset($config->action->objectIdFields[$module]) ? $config->action->objectIdFields[$module] : 'id';
|
||||
$titleField = isset($config->action->objectNameFields[$module]) ? $config->action->objectNameFields[$module] : 'title';
|
||||
|
||||
return array(zget($object, $titleField, ''), zget($object, $idField, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Init table data of zin.
|
||||
*
|
||||
* @param array $items
|
||||
* @param array $fieldList
|
||||
* @param object $model
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function initTableData(array $items, array &$fieldList, object $model = null): array
|
||||
{
|
||||
$items = setParent($items);
|
||||
if(empty($fieldList['actions'])) return $items;
|
||||
|
||||
foreach($fieldList['actions']['menu'] as $actionMenu)
|
||||
{
|
||||
if(is_array($actionMenu))
|
||||
{
|
||||
foreach($actionMenu as $actionMenuKey => $actionName)
|
||||
{
|
||||
if($actionMenuKey == 'other')
|
||||
{
|
||||
foreach($actionName as $otherActionName) initTableActions($fieldList, $otherActionName);
|
||||
}
|
||||
else
|
||||
{
|
||||
initTableActions($fieldList, $actionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
initTableActions($fieldList, $actionMenu);
|
||||
}
|
||||
}
|
||||
|
||||
global $app;
|
||||
if(empty($model))
|
||||
{
|
||||
$module = $app->getModuleName();
|
||||
$model = $app->control->loadModel($module);
|
||||
}
|
||||
|
||||
$maxActionCount = 0;
|
||||
foreach($items as $item)
|
||||
{
|
||||
$item->actions = array();
|
||||
foreach($fieldList['actions']['menu'] as $actionKey => $actionMenu)
|
||||
{
|
||||
if(isset($actionMenu['other']))
|
||||
{
|
||||
$currentActionMenu = $actionMenu[0];
|
||||
initItemActions($item, $currentActionMenu, $fieldList['actions']['list'], $model);
|
||||
|
||||
$otherActionMenus = $actionMenu['other'];
|
||||
$otherAction = '';
|
||||
foreach($otherActionMenus as $otherActionMenu)
|
||||
{
|
||||
$otherActions = explode('|', $otherActionMenu);
|
||||
foreach($otherActions as $otherActionName)
|
||||
{
|
||||
if(in_array($otherActionName, array_column($item->actions, 'name'))) continue;
|
||||
|
||||
if(method_exists($model, 'isClickable') && !$model->isClickable($item, $otherActionName)) $otherAction .= '-';
|
||||
$otherAction .= $otherActionName . ',';
|
||||
}
|
||||
}
|
||||
$item->actions[] = 'other:' . $otherAction;
|
||||
}
|
||||
elseif($actionKey == 'more')
|
||||
{
|
||||
$moreAction = '';
|
||||
foreach($actionMenu as $moreActionName)
|
||||
{
|
||||
if(method_exists($model, 'isClickable') && !$model->isClickable($item, $moreActionName)) $moreAction .= '-';
|
||||
$moreAction .= $moreActionName . ',';
|
||||
}
|
||||
|
||||
$item->actions[] = 'more:' . $moreAction;
|
||||
}
|
||||
elseif(is_array($actionMenu)) // Two or more grups.
|
||||
{
|
||||
/*
|
||||
* Menu可能会有多套,如果只有一套可以直接用一维数组。
|
||||
* There are maybe two or more groups of action menus.
|
||||
*/
|
||||
$item->actions = array();
|
||||
$isClickable = false;
|
||||
foreach($actionMenu as $actionName) $isClickable |= initItemActions($item, $actionName, $fieldList['actions']['list'], $model);
|
||||
|
||||
if($isClickable) break; // If the action is clickable, use this group.
|
||||
}
|
||||
else // Only one group of action menus.
|
||||
{
|
||||
initItemActions($item, $actionMenu, $fieldList['actions']['list'], $model);
|
||||
}
|
||||
}
|
||||
|
||||
if(count($item->actions) > $maxActionCount) $maxActionCount = count($item->actions);
|
||||
}
|
||||
if(isset($fieldList['actions'])) $fieldList['actions']['minWidth'] = $maxActionCount * 24 + 24;
|
||||
|
||||
return array_values($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent property of the data.
|
||||
*
|
||||
* @param array $items
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function setParent(array $items)
|
||||
{
|
||||
foreach($items as $item)
|
||||
{
|
||||
/* Set parent attribute. */
|
||||
$item->isParent = false;
|
||||
if(isset($item->parent) && $item->parent == -1)
|
||||
{
|
||||
/* When the parent is -1, the hierarchical structure is displayed incorrectly. */
|
||||
$item->parent = 0;
|
||||
$item->isParent = true;
|
||||
}
|
||||
|
||||
if(!empty($item->parent) && isset($items[$item->parent])) $items[$item->parent]->isParent = true;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init column actions of a table.
|
||||
*
|
||||
* @param array $fieldList
|
||||
* @param string $actionMenu
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
function initTableActions(array &$fieldList, string $actionMenu): void
|
||||
{
|
||||
$actions = explode('|', $actionMenu);
|
||||
foreach($actions as $action)
|
||||
{
|
||||
if(!isset($fieldList['actions']['list'][$action])) continue;
|
||||
|
||||
$actionConfig = $fieldList['actions']['list'][$action];
|
||||
$actionConfig['text'] = '';
|
||||
|
||||
if(!empty($actionConfig['url']['module']) && !empty($actionConfig['url']['method']))
|
||||
{
|
||||
$module = $actionConfig['url']['module'];
|
||||
$method = $actionConfig['url']['method'];
|
||||
$params = !empty($actionConfig['url']['params']) ? $actionConfig['url']['params'] : array();
|
||||
|
||||
$actionConfig['url'] = helper::createLink($module, $method, $params);
|
||||
}
|
||||
|
||||
$fieldList['actions']['actionsMap'][$action] = $actionConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init row actions of a item.
|
||||
*
|
||||
* @param object $item
|
||||
* @param string $actionMenu
|
||||
* @param array $actionList
|
||||
* @param object $model
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
function initItemActions(object &$item, string $actionMenu, array $actionList, object $model): bool
|
||||
{
|
||||
global $app;
|
||||
$module = $app->getModuleName();
|
||||
$method = '';
|
||||
|
||||
$isClickable = false;
|
||||
$actions = explode('|', $actionMenu);
|
||||
foreach($actions as $action)
|
||||
{
|
||||
if(!isset($actionList[$action])) continue;
|
||||
|
||||
$actionConfig = $actionList[$action];
|
||||
if(!empty($actionConfig['url']['module']) && $module != $actionConfig['url']['module'])
|
||||
{
|
||||
$module = $actionConfig['url']['module'];
|
||||
$model = $app->control->loadModel($module);
|
||||
}
|
||||
|
||||
$method = $action;
|
||||
if(!empty($actionConfig['url']['method']) && $method != $actionConfig['url']['method']) $method = $actionConfig['url']['method'];
|
||||
|
||||
if(!method_exists($model, 'isClickable') || $model->isClickable($item, $method))
|
||||
{
|
||||
$isClickable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$method || !common::hasPriv($module, $method)) return $isClickable;
|
||||
|
||||
$item->actions[] = array('name' => $action, 'disabled' => !$isClickable);
|
||||
|
||||
return $isClickable;
|
||||
}
|
||||
|
||||
@@ -1160,18 +1160,8 @@ EOT;
|
||||
return $js;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出$config到js,因为js的createLink()方法需要获取config信息。
|
||||
* Export the config vars for createLink() js version.
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
static public function exportConfigVars()
|
||||
static function getJSConfigVars()
|
||||
{
|
||||
if(!function_exists('json_encode')) return false;
|
||||
|
||||
global $app, $config, $lang;
|
||||
$defaultViewType = $app->getViewType();
|
||||
$themeRoot = $app->getWebRoot() . 'theme/';
|
||||
@@ -1180,7 +1170,7 @@ EOT;
|
||||
$clientLang = $app->getClientLang();
|
||||
$runMode = defined('RUN_MODE') ? RUN_MODE : '';
|
||||
$requiredFields = '';
|
||||
if(isset($config->$moduleName->$methodName->requiredFields)) $requiredFields = str_replace(' ', '', $config->$moduleName->$methodName->requiredFields);
|
||||
if(isset($config->$moduleName->$methodName->requiredFields)) $requiredFields = str_replace(' ', '', (string) $config->$moduleName->$methodName->requiredFields);
|
||||
|
||||
$jsConfig = new stdclass();
|
||||
$jsConfig->webRoot = $config->webRoot;
|
||||
@@ -1199,21 +1189,40 @@ EOT;
|
||||
$jsConfig->clientLang = $clientLang;
|
||||
$jsConfig->requiredFields = $requiredFields;
|
||||
$jsConfig->router = $app->server->SCRIPT_NAME;
|
||||
$jsConfig->save = isset($lang->save) ? $lang->save : '';
|
||||
$jsConfig->save = $lang->save ?? '';
|
||||
$jsConfig->runMode = $runMode;
|
||||
$jsConfig->timeout = isset($config->timeout) ? $config->timeout : '';
|
||||
$jsConfig->pingInterval = isset($config->pingInterval) ? $config->pingInterval : '';
|
||||
$jsConfig->timeout = $config->timeout ?? '';
|
||||
$jsConfig->pingInterval = $config->pingInterval ?? '';
|
||||
$jsConfig->onlybody = zget($_GET, 'onlybody', 'no');
|
||||
$jsConfig->tabSession = $config->tabSession;
|
||||
if($config->tabSession and helper::isWithTID()) $jsConfig->tid = zget($_GET, 'tid', '');
|
||||
|
||||
return $jsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出$config到js,因为js的createLink()方法需要获取config信息。
|
||||
* Export the config vars for createLink() js version.
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
static public function exportConfigVars()
|
||||
{
|
||||
if(!function_exists('json_encode')) return false;
|
||||
|
||||
global $lang;
|
||||
|
||||
$jsConfig = static::getJSConfigVars();
|
||||
|
||||
$jsLang = new stdclass();
|
||||
$jsLang->submitting = isset($lang->loading) ? $lang->loading : '';
|
||||
$jsLang->submitting = $lang->loading ?? '';
|
||||
$jsLang->save = $jsConfig->save;
|
||||
$jsLang->expand = isset($lang->expand) ? $lang->expand : '';
|
||||
$jsLang->timeout = isset($lang->timeout) ? $lang->timeout : '';
|
||||
$jsLang->confirmDraft = isset($lang->confirmDraft) ? $lang->confirmDraft : '';
|
||||
$jsLang->resume = isset($lang->resume) ? $lang->resume : '';
|
||||
$jsLang->expand = $lang->expand ?? '';
|
||||
$jsLang->timeout = $lang->timeout ?? '';
|
||||
$jsLang->confirmDraft = $lang->confirmDraft ?? '';
|
||||
$jsLang->resume = $lang->resume ?? '';
|
||||
$jsLang->program = zget($lang->program, 'common', '');
|
||||
$jsLang->project = zget($lang->project, 'common', '');
|
||||
$jsLang->product = zget($lang->product, 'common', '');
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The trace class file of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author Lu Fei <lufei@easycorp.ltd>
|
||||
* @package trace
|
||||
* @link https://www.zentao.net
|
||||
*/
|
||||
|
||||
class trace
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $trace = array();
|
||||
|
||||
protected $app;
|
||||
|
||||
protected $dao;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $app, $dao;
|
||||
$this->app = $app;
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求信息。
|
||||
* Get request info.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getRequestInfo()
|
||||
{
|
||||
$this->trace['request'] = array(
|
||||
'start' => date('Y-m-d H:i:s', (int)$this->app->startTime),
|
||||
'url' => $this->app->getURI(true),
|
||||
'protocol' => $this->app->server->server_protocol,
|
||||
'method' => $this->app->server->request_method,
|
||||
'timeUsed' => round(getTime() - $this->app->startTime, 4) * 1000,
|
||||
'memory' => round(memory_get_peak_usage() / 1024, 1),
|
||||
'querys' => count(dao::$querys),
|
||||
'caches' => count(dao::$cache),
|
||||
'files' => count(get_included_files()),
|
||||
'session' => session_id()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求加载的文件。
|
||||
* Get request files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getRequestFiles()
|
||||
{
|
||||
$this->trace['files'] = get_included_files();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求的 SQL 语句。
|
||||
* Get request SQLs.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getRequestSqls()
|
||||
{
|
||||
$this->trace['sqlQuery'] = dao::$querys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求的 SQL profiles。
|
||||
* Get request SQL profiles.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSQLProfiles()
|
||||
{
|
||||
$profiling = $this->dao->dbh->query('SHOW PROFILES')->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$this->trace['profiles'] = $profiling;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求 Trace。
|
||||
* Generate request trace.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTrace()
|
||||
{
|
||||
$this->getRequestInfo();
|
||||
$this->getRequestFiles();
|
||||
$this->getRequestSqls();
|
||||
$this->getSQLProfiles();
|
||||
return $this->trace;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode($this->getTrace());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The config 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;
|
||||
|
||||
function loadConfig()
|
||||
{
|
||||
global $app, $config;
|
||||
|
||||
$config->zin = new \stdClass();
|
||||
|
||||
$config->zin->lang = $app->getClientLang();
|
||||
$config->zin->wgVer = isset($config->wgVer) ? $config->wgVer : '1';
|
||||
$config->zin->wgVerMap = isset($config->wgVerMap) ? $config->wgVerMap : array();
|
||||
$config->zin->zuiPath = isset($config->zuiPath) ? $config->zuiPath : ($app->getWebRoot() . 'js/zui3/');
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?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;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'dataset.class.php';
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php';
|
||||
|
||||
class context extends \zin\utils\dataset
|
||||
{
|
||||
public function addImport()
|
||||
{
|
||||
return $this->addToList('import', func_get_args());
|
||||
}
|
||||
|
||||
public function getImportList()
|
||||
{
|
||||
return $this->getList('import');
|
||||
}
|
||||
|
||||
public function addCSS()
|
||||
{
|
||||
return $this->addToList('css', func_get_args());
|
||||
}
|
||||
|
||||
public function getCSS()
|
||||
{
|
||||
return trim(implode("\n", $this->getList('css')));
|
||||
}
|
||||
|
||||
public function addJS()
|
||||
{
|
||||
return $this->addToList('js', func_get_args());
|
||||
}
|
||||
|
||||
public function addJSVar($name, $value)
|
||||
{
|
||||
return $this->addToList('jsVar', h::createJsVarCode($name, $value));
|
||||
}
|
||||
|
||||
public function addWgWithEvents($wg)
|
||||
{
|
||||
$list = $this->getWgWithEventsList();
|
||||
if(in_array($wg, $list)) return $this;
|
||||
return $this->addToList('wgWithEvents', $wg);
|
||||
}
|
||||
|
||||
public function getWgWithEventsList()
|
||||
{
|
||||
return $this->getList('wgWithEvents');
|
||||
}
|
||||
|
||||
public function addJSCall()
|
||||
{
|
||||
$code = call_user_func_array('\zin\h::createJsCallCode', func_get_args());
|
||||
return $this->addToList('jsCall', $code);
|
||||
}
|
||||
|
||||
public function getEventsBindings()
|
||||
{
|
||||
$wgs = $this->getList('wgWithEvents');
|
||||
$codes = [];
|
||||
foreach($wgs as $wg)
|
||||
{
|
||||
if(!method_exists($wg, 'buildEvents')) continue;
|
||||
$code = $wg->buildEvents();
|
||||
if(!empty($code)) $codes[] = $code;
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
public function getJS()
|
||||
{
|
||||
$js = trim(implode("\n", array_merge($this->getList('jsVar'), $this->getList('js'), $this->getEventsBindings(), $this->getList('jsCall'))));
|
||||
if(empty($js)) return '';
|
||||
|
||||
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) $js .= "if(typeof $method === 'function') window.$method = $method;";
|
||||
}
|
||||
return $js;
|
||||
}
|
||||
|
||||
public static $map = array();
|
||||
|
||||
public static function js(/* string ...$code */)
|
||||
{
|
||||
$context = static::current();
|
||||
call_user_func_array(array($context, 'addJS'), \zin\utils\flat(func_get_args()));
|
||||
}
|
||||
|
||||
public static function jsCall(/* string ...$code */)
|
||||
{
|
||||
$context = static::current();
|
||||
call_user_func_array(array($context, 'addJSCall'), func_get_args());
|
||||
}
|
||||
|
||||
|
||||
public static function jsVar($name, $value)
|
||||
{
|
||||
$context = static::current();
|
||||
$context->addJSVar($name, $value);
|
||||
}
|
||||
|
||||
public static function css(/* string ...$code */)
|
||||
{
|
||||
$context = static::current();
|
||||
call_user_func_array(array($context, 'addCSS'), \zin\utils\flat(func_get_args()));
|
||||
}
|
||||
|
||||
public static function import(/* string ...$files */)
|
||||
{
|
||||
$context = static::current();
|
||||
call_user_func_array(array($context, 'addImport'), func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current context.
|
||||
*
|
||||
* @access public
|
||||
* @return context
|
||||
*/
|
||||
public static function current(): context
|
||||
{
|
||||
if(empty(static::$map)) static::$map['current'] = new context(null);
|
||||
return static::$map['current'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create widget context.
|
||||
*
|
||||
* @access public
|
||||
* @param string $gid The widget gid.
|
||||
* @return context
|
||||
*/
|
||||
public static function create(string $gid): context
|
||||
{
|
||||
if(isset(static::$map[$gid])) return static::$map[$gid];
|
||||
$context = new context();
|
||||
static::$map[$gid] = $context;
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy widget context.
|
||||
*
|
||||
* @access public
|
||||
* @param string $gid The widget gid.
|
||||
* @return void
|
||||
*/
|
||||
public static function destroy(string $gid = null): void
|
||||
{
|
||||
if($gid === null) unset(static::$map['current']);
|
||||
elseif(isset(static::$map[$gid])) unset(static::$map[$gid]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The context function 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 . 'context.class.php';
|
||||
|
||||
function js()
|
||||
{
|
||||
call_user_func_array('\zin\context::js', func_get_args());
|
||||
}
|
||||
|
||||
function jsCall()
|
||||
{
|
||||
call_user_func_array('\zin\context::jsCall', func_get_args());
|
||||
}
|
||||
|
||||
function jsVar()
|
||||
{
|
||||
call_user_func_array('\zin\context::jsVar', func_get_args());
|
||||
}
|
||||
|
||||
function css()
|
||||
{
|
||||
call_user_func_array('\zin\context::css', func_get_args());
|
||||
}
|
||||
|
||||
function import()
|
||||
{
|
||||
call_user_func_array('\zin\context::import', func_get_args());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The data function 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;
|
||||
|
||||
function setPageData($name, $value)
|
||||
{
|
||||
if(is_array($value) && empty($name))
|
||||
{
|
||||
foreach ($value as $key => $val) zin::setData($key, $val);
|
||||
return;
|
||||
}
|
||||
zin::setData($name, $value);
|
||||
}
|
||||
|
||||
function getPageData($name)
|
||||
{
|
||||
if(is_array($name))
|
||||
{
|
||||
$values = array();
|
||||
foreach($name as $propName)
|
||||
{
|
||||
$values[] = zin::getData($propName);
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
return zin::getData($name);
|
||||
}
|
||||
|
||||
function data(...$args)
|
||||
{
|
||||
if(count($args) >= 2) return setPageData($args[0], $args[1]);
|
||||
return getPageData($args[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page data
|
||||
* @deprecated Use data($name, $value) insteadOf useData($name, $value)
|
||||
*/
|
||||
function useData($name, $value)
|
||||
{
|
||||
return setPageData($name, $value);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The directive class file of zin lib.
|
||||
*
|
||||
* @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 . 'zin.class.php';
|
||||
|
||||
class directive
|
||||
{
|
||||
public string $type;
|
||||
|
||||
public mixed $data;
|
||||
|
||||
public ?array $options;
|
||||
|
||||
public ?wg $parent = null;
|
||||
|
||||
/**
|
||||
* Construct a directive object
|
||||
* @param string $type
|
||||
* @param mixed $data
|
||||
* @param array $options
|
||||
* @access public
|
||||
*/
|
||||
public function __construct(string $type, mixed $data, ?array $options = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
$this->options = $options;
|
||||
|
||||
zin::renderInGlobal($this);
|
||||
}
|
||||
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return array(
|
||||
'type' => $this->type,
|
||||
'data' => $this->data,
|
||||
'options' => $this->options
|
||||
);
|
||||
}
|
||||
|
||||
public static function is(mixed $item, ?string $type = null): bool
|
||||
{
|
||||
return $item instanceof directive && ($type === null || $item->type === $type);
|
||||
}
|
||||
}
|
||||
|
||||
function directive($type, $data, $options = null): directive
|
||||
{
|
||||
return new directive($type, $data, $options);
|
||||
}
|
||||
|
||||
function isDirective(mixed $item, ?string $type = null): bool
|
||||
{
|
||||
return directive::is($item, $type);
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The dom widget 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 stdClass;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'deep.func.php';
|
||||
require_once __DIR__ . DS . 'selector.func.php';
|
||||
|
||||
class dom
|
||||
{
|
||||
/**
|
||||
* @var wg
|
||||
*/
|
||||
public $wg;
|
||||
|
||||
public $children = array();
|
||||
|
||||
public $selectors = null;
|
||||
|
||||
public $renderInner = false;
|
||||
|
||||
public $renderType;
|
||||
|
||||
public $dataGetters = null;
|
||||
|
||||
public $dataCommands;
|
||||
|
||||
public $buildList = null;
|
||||
|
||||
public $buildListInner = false;
|
||||
|
||||
/**
|
||||
* Construct the dom object.
|
||||
*
|
||||
* @param wg $wg
|
||||
* @param array $children
|
||||
* @param array|string|object $selectors
|
||||
* @access public
|
||||
*/
|
||||
public function __construct($wg, $children, $selectors = null, $renderType = null, $dataCommands = null)
|
||||
{
|
||||
$this->wg = $wg;
|
||||
$this->renderType = $renderType;
|
||||
|
||||
$this->add($children);
|
||||
$this->addSelectors($selectors);
|
||||
$this->addDataCommands($dataCommands);
|
||||
}
|
||||
|
||||
public function __debugInfo()
|
||||
{
|
||||
return array(
|
||||
'gid' => $this->wg->gid,
|
||||
'type' => $this->wg->type(),
|
||||
'count' => count($this->children),
|
||||
'renderInner' => $this->renderInner,
|
||||
'renderType' => $this->renderType,
|
||||
'dataCommands' => $this->dataCommands,
|
||||
'selectors' => stringifyWgSelectors($this->selectors)
|
||||
);
|
||||
}
|
||||
|
||||
public function add($children)
|
||||
{
|
||||
if(empty($children)) return;
|
||||
|
||||
if(!is_array($children)) $children = [$children];
|
||||
foreach($children as $child)
|
||||
{
|
||||
if(is_array($child)) $this->add($child);
|
||||
else $this->children[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
public function addDataCommands($commands)
|
||||
{
|
||||
if(empty($commands)) return;
|
||||
|
||||
if(is_string($commands))
|
||||
{
|
||||
$commandList = explode(',', $commands);
|
||||
$commands = array();
|
||||
foreach($commandList as $command)
|
||||
{
|
||||
$parts = explode(':', $command, 2);
|
||||
$commands[$parts[0]] = count($parts) > 1 ? $parts[1] : $parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
if($this->dataCommands === null) $this->dataCommands = array();
|
||||
$index = 0;
|
||||
foreach($commands as $key => $command)
|
||||
{
|
||||
$this->dataCommands[$index == $key ? $command : $key] = $command;
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
|
||||
public function addSelectors($selectors)
|
||||
{
|
||||
if(empty($selectors)) return;
|
||||
|
||||
if($this->selectors === null) $this->selectors = array();
|
||||
$selectors = parseWgSelectors($selectors);
|
||||
foreach($selectors as $selector)
|
||||
{
|
||||
if(isset($selector->command) && !empty($selector->command)) $this->addDataCommands([$selector->tag => $selector->command]);
|
||||
else $this->selectors[] = $selector;
|
||||
}
|
||||
}
|
||||
|
||||
public function isMatch($selector)
|
||||
{
|
||||
return $this->wg->isMatch($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the children dom list.
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
if($this->buildList !== null && $this->buildListInner === $this->renderInner) return $this->buildList;
|
||||
|
||||
if(empty($this->selectors) && !empty($this->dataCommands))
|
||||
{
|
||||
$this->buildList = array();
|
||||
return $this->buildList;
|
||||
}
|
||||
|
||||
$list = array();
|
||||
$children = $this->renderInner ? $this->wg->children() : $this->children;
|
||||
|
||||
if(empty($children)) return $list;
|
||||
|
||||
foreach($children as $child) $list[] = ($child instanceof wg) ? $child->buildDom() : $child;
|
||||
|
||||
if(!empty($this->selectors)) $list = static::filter($list, $this->selectors);
|
||||
|
||||
$this->buildList = $list;
|
||||
$this->buildListInner = $this->renderInner;
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
if($this->renderType === 'json') return $this->renderJson();
|
||||
if($this->renderType === 'list') return $this->renderList();
|
||||
return $this->renderHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dom to json object.
|
||||
*
|
||||
* @access public
|
||||
* @return object
|
||||
*/
|
||||
public function renderJson(): object
|
||||
{
|
||||
$list = $this->build();
|
||||
$output = new stdClass();
|
||||
foreach($list as $name => $item)
|
||||
{
|
||||
$output->$name = static::renderItemToJson($item);
|
||||
}
|
||||
|
||||
if(!empty($this->dataCommands))
|
||||
{
|
||||
$data = array();
|
||||
foreach($this->dataCommands as $name => $command)
|
||||
{
|
||||
$data[$name] = data($command);
|
||||
}
|
||||
$output->data = $data;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dom to html string.
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function renderHtml(): string
|
||||
{
|
||||
$list = $this->build();
|
||||
if(empty($list)) return '';
|
||||
|
||||
$output = array();
|
||||
foreach($list as $item)
|
||||
{
|
||||
$result = static::renderItemToHtml($item);
|
||||
if(!is_string($result)) $result = json_encode($result);
|
||||
$output[] = $result;
|
||||
}
|
||||
return implode('', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dom to list by given selector.
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function renderList(): array
|
||||
{
|
||||
$list = $this->build();
|
||||
$output = array();
|
||||
foreach($list as $name => $item)
|
||||
{
|
||||
if(is_array($item) && count($item) === 1) $item = $item[0];
|
||||
$renderType = $item instanceof dom ? $item->renderType : 'html';
|
||||
if(empty($renderType)) $renderType = 'html';
|
||||
$output[] = array('name' => $name, 'data' => static::renderDomItem($item, $renderType), 'type' => $renderType);
|
||||
}
|
||||
|
||||
if(!empty($this->dataCommands))
|
||||
{
|
||||
foreach($this->dataCommands as $name => $command)
|
||||
{
|
||||
$output[] = array('name' => $name, 'data' => data($command), 'type' => 'command');
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function renderDomItem($item, $defaultType = 'html')
|
||||
{
|
||||
if($item instanceof dom)
|
||||
{
|
||||
$renderType = $item->renderType;
|
||||
if(empty($renderType)) $renderType = $defaultType;
|
||||
if($renderType === 'json') return dom::renderItemToJson($item);
|
||||
return dom::renderItemToHtml($item->build());
|
||||
}
|
||||
|
||||
$renderType = $defaultType;
|
||||
if($renderType === 'json') return static::renderItemToJson($item);
|
||||
return static::renderItemToHtml($item);
|
||||
}
|
||||
|
||||
public static function renderItemToJson($item)
|
||||
{
|
||||
if($item === null || is_bool($item)) return null;
|
||||
|
||||
if(is_array($item))
|
||||
{
|
||||
$output = array();
|
||||
foreach($item as $subItem) $output[] = static::renderItemToJson($subItem);
|
||||
return $output;
|
||||
}
|
||||
|
||||
if($item instanceof dom)
|
||||
{
|
||||
$json = $item->wg->toJsonData();
|
||||
if(!empty($item->dataGetters))
|
||||
{
|
||||
$output = array();
|
||||
$props = explode(',', $item->dataGetters);
|
||||
foreach($props as $prop)
|
||||
{
|
||||
$prop = trim($prop);
|
||||
if(empty($prop)) continue;
|
||||
|
||||
$parts = explode(':', $prop, 2);
|
||||
$name = $parts[0];
|
||||
$namePath = count($parts) > 1 ? $parts[1] : $parts[0];
|
||||
$output[$name] = \zin\utils\deepGet($json, $namePath);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
if($item instanceof wg) return dom::renderDomItem($item, 'json');
|
||||
if(is_string($item)) return $item;
|
||||
|
||||
if(is_object($item))
|
||||
{
|
||||
if(isDirective($item, 'html')) return $item->data;
|
||||
if(isDirective($item, 'text')) return htmlspecialchars($item->data);
|
||||
if(isset($item->html)) return $item->html;
|
||||
if(isset($item->text)) return htmlspecialchars($item->text);
|
||||
if(method_exists($item, 'render')) return $item->render();
|
||||
}
|
||||
|
||||
return strval($item);
|
||||
}
|
||||
|
||||
public static function renderItemToHtml($item)
|
||||
{
|
||||
if($item === null || is_bool($item)) return '';
|
||||
|
||||
if(is_array($item))
|
||||
{
|
||||
$output = array();
|
||||
foreach($item as $subItem) $output[] = static::renderItemToHtml($subItem);
|
||||
return implode('', $output);
|
||||
}
|
||||
|
||||
if($item instanceof dom) return dom::renderItemToHtml($item->build());
|
||||
if($item instanceof wg) return $item->render();
|
||||
if(is_string($item)) return $item;
|
||||
|
||||
if(is_object($item))
|
||||
{
|
||||
if(isDirective($item, 'html')) return $item->data;
|
||||
if(isDirective($item, 'text')) return htmlspecialchars($item->data);
|
||||
if(isset($item->html)) return $item->html;
|
||||
if(isset($item->text)) return htmlspecialchars($item->text);
|
||||
if(method_exists($item, 'render')) return $item->render();
|
||||
}
|
||||
|
||||
return strval($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the dom list with selector.
|
||||
*
|
||||
* @param array $list
|
||||
* @param object $selector
|
||||
* @param array $filteredList
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function filterList(&$list, $selector, &$filteredList)
|
||||
{
|
||||
if(empty($list) || empty($selector)) return [];
|
||||
|
||||
$results = array();
|
||||
foreach($list as $item)
|
||||
{
|
||||
if(!($item instanceof dom) || in_array($item->wg->gid, $filteredList)) continue;
|
||||
|
||||
if($item->wg->isMatch($selector))
|
||||
{
|
||||
$item->selector = $selector;
|
||||
$item->renderInner = isset($selector->inner) ? $selector->inner : false;
|
||||
$item->renderType = isset($selector->type) ? $selector->type : null;
|
||||
$item->dataGetters = isset($selector->data) ? $selector->data : null;
|
||||
|
||||
$filteredList[] = $item->wg->gid;
|
||||
$results[] = $item;
|
||||
}
|
||||
else
|
||||
{
|
||||
$children = $item->build();
|
||||
if(!empty($children))
|
||||
{
|
||||
$subResults = static::filterList($children, $selector, $filteredList);
|
||||
foreach($subResults as $subItem) $results[] = $subItem;
|
||||
}
|
||||
}
|
||||
if($selector->first && !empty($results)) break;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the dom list with selectors.
|
||||
*
|
||||
* @param array $domList
|
||||
* @param array $selectors
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function filter(&$domList, $selectors)
|
||||
{
|
||||
if(empty($selectors)) return $domList;
|
||||
|
||||
$list = array();
|
||||
$filteredList = array();
|
||||
foreach($selectors as $selector)
|
||||
{
|
||||
$results = static::filterList($domList, $selector, $filteredList);
|
||||
$list[$selector->name] = $results;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The html element 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 dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php';
|
||||
require_once __DIR__ . DS . 'wg.class.php';
|
||||
require_once __DIR__ . DS . 'wg.func.php';
|
||||
|
||||
class h extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'tagName: string',
|
||||
'selfClose?: bool'
|
||||
);
|
||||
|
||||
public function getTagName(): string
|
||||
{
|
||||
return $this->props->get('tagName');
|
||||
}
|
||||
|
||||
public function isDomElement(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isSelfClose()
|
||||
{
|
||||
$selfClose = $this->props->get('selfClose');
|
||||
if($selfClose !== null) return $selfClose;
|
||||
|
||||
return in_array($this->getTagName(), static::$selfCloseTags);
|
||||
}
|
||||
|
||||
public function build(): array
|
||||
{
|
||||
if($this->isSelfClose()) return array($this->buildSelfCloseTag());
|
||||
|
||||
return array($this->buildTagBegin(), parent::build(), $this->buildTagEnd());
|
||||
}
|
||||
|
||||
public function toJsonData(): array
|
||||
{
|
||||
$data = parent::toJsonData();
|
||||
$data['type'] = 'h:' . $this->getTagName();
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return $this->getTagName();
|
||||
}
|
||||
|
||||
public function shortType(): string
|
||||
{
|
||||
return $this->getTagName();
|
||||
}
|
||||
|
||||
protected function getPropsStr(): string
|
||||
{
|
||||
$propStr = $this->props->toStr(array_keys(static::definedPropsList()));
|
||||
if($this->props->hasEvent() && empty($this->id()) && $this->getTagName() !== 'html') $propStr = "$propStr id='$this->gid'";
|
||||
return empty($propStr) ? '' : " $propStr";
|
||||
}
|
||||
|
||||
protected function buildSelfCloseTag(): string
|
||||
{
|
||||
$tagName = $this->getTagName();
|
||||
$propStr = $this->getPropsStr();
|
||||
return "<$tagName$propStr />";
|
||||
}
|
||||
|
||||
protected function buildTagBegin(): string
|
||||
{
|
||||
$tagName = $this->getTagName();
|
||||
$propStr = $this->getPropsStr();
|
||||
return "<$tagName$propStr>";
|
||||
}
|
||||
|
||||
protected function buildTagEnd(): string
|
||||
{
|
||||
$tagName = $this->getTagName();
|
||||
return "</$tagName>";
|
||||
}
|
||||
|
||||
public static function create(): h
|
||||
{
|
||||
$args = func_get_args();
|
||||
$tagName = array_shift($args);
|
||||
return new h(is_string($tagName) ? set('tagName', $tagName) : $tagName, $args);
|
||||
}
|
||||
|
||||
public static function __callStatic(string $tagName, array $args): h
|
||||
{
|
||||
return new h(set('tagName', $tagName), $args);
|
||||
}
|
||||
|
||||
public static function a(): h
|
||||
{
|
||||
$a = static::create('a', func_get_args());
|
||||
if($a->prop('target') === '_blank' && !$a->hasProp('rel')) $a->prop('rel', 'noopener noreferrer');
|
||||
return $a;
|
||||
}
|
||||
|
||||
public static function button()
|
||||
{
|
||||
return static::create('button', set('type', 'button'), func_get_args());
|
||||
}
|
||||
|
||||
public static function input()
|
||||
{
|
||||
return static::create('input', set('type', 'text'), func_get_args());
|
||||
}
|
||||
|
||||
public static function formHidden($name, $value, ...$args)
|
||||
{
|
||||
return static::create('input', set('type', 'hidden'), set::name($name), set::value($value), $args);
|
||||
}
|
||||
|
||||
public static function checkbox()
|
||||
{
|
||||
return static::create('input', set('type', 'checkbox'), func_get_args());
|
||||
}
|
||||
|
||||
public static function radio()
|
||||
{
|
||||
return static::create('input', set('type', 'radio'), func_get_args());
|
||||
}
|
||||
|
||||
public static function date()
|
||||
{
|
||||
return static::create('input', set('type', 'date'), func_get_args());
|
||||
}
|
||||
|
||||
public static function file()
|
||||
{
|
||||
return static::create('input', set('type', 'file'), func_get_args());
|
||||
}
|
||||
|
||||
public static function textarea(...$args)
|
||||
{
|
||||
list($code, $args) = h::splitRawCode($args);
|
||||
return static::create('textarea', $code, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a html comment tag <!--...-->
|
||||
*
|
||||
* @access public
|
||||
* @param string $comment
|
||||
* @return directive
|
||||
*/
|
||||
public static function comment(string $comment): directive
|
||||
{
|
||||
return html("<!-- $comment -->");
|
||||
}
|
||||
|
||||
public static function importJs($src, ...$args)
|
||||
{
|
||||
return static::create('script', set('src', $src), ...$args);
|
||||
}
|
||||
|
||||
public static function importCss($src, ...$args)
|
||||
{
|
||||
return static::create('link', set('rel', 'stylesheet'), set('href', $src), ...$args);
|
||||
}
|
||||
|
||||
public static function import($file, $type = null, ...$args)
|
||||
{
|
||||
if(is_array($file))
|
||||
{
|
||||
$children = array();
|
||||
foreach($file as $file)
|
||||
{
|
||||
$children[] = static::import($file, $type);
|
||||
}
|
||||
return $children;
|
||||
}
|
||||
if($type === null) $type = pathinfo($file, PATHINFO_EXTENSION);
|
||||
if($type == 'js' || $type == 'cjs') return static::importJs($file, ...$args);
|
||||
if($type == 'css') return static::importCss($file, ...$args);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function css(...$args)
|
||||
{
|
||||
list($code, $args) = h::splitRawCode($args);
|
||||
if(empty($code)) return null;
|
||||
return static::create('style', html(implode("\n", $code)), ...$args);
|
||||
}
|
||||
|
||||
public static function globalJS(...$args)
|
||||
{
|
||||
list($code, $args) = h::splitRawCode($args);
|
||||
if(empty($code)) return null;
|
||||
return static::create('script', html(implode("\n", $code)), ...$args);
|
||||
}
|
||||
|
||||
public static function js(...$args)
|
||||
{
|
||||
|
||||
list($code, $args) = h::splitRawCode($args);
|
||||
if(empty($code)) return null;
|
||||
return static::create('script', html(h::createJsScopeCode($code)), ...$args);
|
||||
}
|
||||
|
||||
public static function jsVar($name, $value, ...$directives)
|
||||
{
|
||||
return static::js(static::createJsVarCode($name, $value), ...$directives);
|
||||
}
|
||||
|
||||
public static function jsCall($funcName, ...$args)
|
||||
{
|
||||
$funcArgs = [];
|
||||
$directives = [];
|
||||
foreach($args as $arg)
|
||||
{
|
||||
if(isDirective($arg)) $directives[] = $arg;
|
||||
else $funcArgs[] = $arg;
|
||||
}
|
||||
$code = static::createJsCallCode($funcName, $funcArgs);
|
||||
return static::js($code, ...$directives);
|
||||
}
|
||||
|
||||
public static function createJsCallCode($func, $args)
|
||||
{
|
||||
foreach($args as $index => $arg)
|
||||
{
|
||||
$args[$index] = h::encodeJsonWithRawJs($arg, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
if($func[0] === '~')
|
||||
{
|
||||
$func = substr($func, 1);
|
||||
return "$(() => $func(" . implode(',', $args) . "));";
|
||||
}
|
||||
return $func . '(' . implode(',', $args) . ');';
|
||||
}
|
||||
|
||||
public static function createJsVarCode($name, $value)
|
||||
{
|
||||
$vars = is_string($name) ? array($name => $value) : $name;
|
||||
$jsCode = '';
|
||||
foreach($vars as $var => $val)
|
||||
{
|
||||
if(empty($var)) continue;
|
||||
|
||||
$val = h::encodeJsonWithRawJs($val);
|
||||
|
||||
if(str_starts_with($var, 'window.')) $jsCode .= "$var=" . $val . ';';
|
||||
elseif(str_starts_with($var, '+')) $jsCode .= 'let ' . substr($var, 1) . '=' . $val . ';';
|
||||
else $jsCode .= "const $var=" . $val . ';';
|
||||
}
|
||||
return $jsCode;
|
||||
}
|
||||
|
||||
public static function createJsScopeCode(string|array $codes): string
|
||||
{
|
||||
if(is_array($codes)) $codes = implode("\n", $codes);
|
||||
return ";(function(){\n$codes\n}());";
|
||||
}
|
||||
|
||||
public static function jsRaw(): string
|
||||
{
|
||||
return 'RAWJS<' . implode("\n", func_get_args()) . '>RAWJS';
|
||||
}
|
||||
|
||||
protected static function encodeJsonWithRawJs($data)
|
||||
{
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
if(empty($json) && (is_array($data) || is_object($data))) return '[]';
|
||||
|
||||
$json = str_replace('"RAWJS<', '', str_replace('>RAWJS"', '', $json));
|
||||
return $json;
|
||||
}
|
||||
|
||||
protected static function splitRawCode($children)
|
||||
{
|
||||
$children = \zin\utils\flat($children);
|
||||
$code = [];
|
||||
$args = [];
|
||||
foreach($children as $key => $child)
|
||||
{
|
||||
if(is_string($child)) $code[] = $child;
|
||||
else $args[] = $child;
|
||||
}
|
||||
return [$code, $args];
|
||||
}
|
||||
|
||||
public static $selfCloseTags = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr');
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The html helper methods 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 . 'h.class.php';
|
||||
require_once __DIR__ . DS . 'item.class.php';
|
||||
require_once __DIR__ . DS . 'wg.func.php';
|
||||
require_once __DIR__ . DS . 'set.class.php';
|
||||
require_once __DIR__ . DS . 'to.class.php';
|
||||
require_once __DIR__ . DS . 'data.func.php';
|
||||
require_once __DIR__ . DS . 'on.class.php';
|
||||
|
||||
function h(): h {return call_user_func_array('\zin\h::create', func_get_args());}
|
||||
|
||||
function div(): h {return call_user_func_array('\zin\h::div', func_get_args());}
|
||||
function span(): h {return call_user_func_array('\zin\h::span', func_get_args());}
|
||||
function code(): h {return call_user_func_array('\zin\h::code', func_get_args());}
|
||||
function canvas(): h {return call_user_func_array('\zin\h::canvas', func_get_args());}
|
||||
function br(): h {return call_user_func_array('\zin\h::br', func_get_args());}
|
||||
function a(): h {return call_user_func_array('\zin\h::a', func_get_args());}
|
||||
function p(): h {return call_user_func_array('\zin\h::p', func_get_args());}
|
||||
function img(): h {return call_user_func_array('\zin\h::img', func_get_args());}
|
||||
function button(): h {return call_user_func_array('\zin\h::button', func_get_args());}
|
||||
function h1(): h {return call_user_func_array('\zin\h::h1', func_get_args());}
|
||||
function h2(): h {return call_user_func_array('\zin\h::h2', func_get_args());}
|
||||
function h3(): h {return call_user_func_array('\zin\h::h3', func_get_args());}
|
||||
function h4(): h {return call_user_func_array('\zin\h::h4', func_get_args());}
|
||||
function h5(): h {return call_user_func_array('\zin\h::h5', func_get_args());}
|
||||
function h6(): h {return call_user_func_array('\zin\h::h6', func_get_args());}
|
||||
function ul(): h {return call_user_func_array('\zin\h::ul', func_get_args());}
|
||||
function li(): h {return call_user_func_array('\zin\h::li', func_get_args());}
|
||||
function template(): h {return call_user_func_array('\zin\h::template', func_get_args());}
|
||||
function formHidden(): h {return call_user_func_array('\zin\h::formHidden', func_get_args());}
|
||||
function fieldset(): h {return call_user_func_array('\zin\h::fieldset', func_get_args());}
|
||||
function legend(): h {return call_user_func_array('\zin\h::legend', func_get_args());}
|
||||
|
||||
function jsRaw(): string {return call_user_func_array('\zin\h::jsRaw', func_get_args());}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The common item element 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 . 'wg.class.php';
|
||||
require_once __DIR__ . DS . 'wg.func.php';
|
||||
|
||||
class item extends wg
|
||||
{
|
||||
public function build(): wg
|
||||
{
|
||||
if($this->parent instanceof wg && method_exists($this->parent, 'onBuildItem'))
|
||||
{
|
||||
return call_user_func(array($this->parent, 'onBuildItem'), $this);
|
||||
}
|
||||
return parent::build();
|
||||
}
|
||||
}
|
||||
|
||||
function item()
|
||||
{
|
||||
return new item(func_get_args());
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The block setter class file of zin lib.
|
||||
*
|
||||
* @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 . 'wg.func.php';
|
||||
|
||||
class on
|
||||
{
|
||||
public static function __callStatic($name, $args)
|
||||
{
|
||||
list($callback, $options) = array_merge($args, array(null));
|
||||
return on($name, $callback, $options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The portal class file of zin lib.
|
||||
*
|
||||
* @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 . 'wg.class.php';
|
||||
|
||||
class portal extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'target:string'
|
||||
);
|
||||
|
||||
public static function __callStatic($name, $args)
|
||||
{
|
||||
return new portal(set('target', $name), $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The props 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 zin\utils\classlist;
|
||||
use zin\utils\style;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'dataset.class.php';
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'classlist.class.php';
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'style.class.php';
|
||||
|
||||
/**
|
||||
* Manage properties for html element and widgets
|
||||
*/
|
||||
class props extends \zin\utils\dataset
|
||||
{
|
||||
/**
|
||||
* Style property
|
||||
*
|
||||
* @access public
|
||||
* @var style
|
||||
*/
|
||||
public style $style;
|
||||
|
||||
/**
|
||||
* Class property
|
||||
*
|
||||
* @access public
|
||||
* @var classlist
|
||||
*/
|
||||
public classlist $class;
|
||||
|
||||
public static array $booleanAttrs = array('allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected');
|
||||
|
||||
/**
|
||||
* Create properties instance
|
||||
*
|
||||
* @access public
|
||||
* @param array $props - Properties list array
|
||||
*/
|
||||
public function __construct(?array $props = null)
|
||||
{
|
||||
$this->style = new \zin\utils\style();
|
||||
$this->class = new \zin\utils\classlist();
|
||||
|
||||
parent::__construct($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for sub class to modify value on setting it
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name or properties list
|
||||
* @param mixed $value - Property value
|
||||
*/
|
||||
protected function setVal(string $prop, mixed $value): props
|
||||
{
|
||||
if($prop === 'class' || $prop === '.') $this->class->set($value);
|
||||
elseif($prop === 'style' || $prop === '~') $this->style->set($value);
|
||||
elseif(str_starts_with($prop, '~')) $this->style->set(substr($prop, 1), $value);
|
||||
elseif($prop === '--') $this->style->cssVar($value);
|
||||
elseif(str_starts_with($prop, '--')) $this->style->cssVar(substr($prop, 2), $value);
|
||||
elseif($prop === '!') $this->hx($value);
|
||||
elseif(str_starts_with($prop, '!')) $this->hx(substr($prop, 1), $value);
|
||||
elseif(str_starts_with($prop, ':')) $this->set('data-' . substr($prop, 1), $value);
|
||||
elseif($prop === '@') $this->bindEvent($value);
|
||||
elseif(str_starts_with($prop, '@')) $this->bindEvent(substr($prop, 1), $value);
|
||||
else parent::setVal($prop, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getVal(string $prop): mixed
|
||||
{
|
||||
if($prop === 'class' || $prop === '.')
|
||||
{
|
||||
if(!$this->class->count()) return null;
|
||||
return $this->class->toStr();
|
||||
}
|
||||
if($prop === 'style' || $prop === '~')
|
||||
{
|
||||
if(!$this->style->count(true)) return null;
|
||||
return $this->style->toStr();
|
||||
}
|
||||
return parent::getVal($prop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function reset(array|string $name, mixed $value = null)
|
||||
{
|
||||
if(is_array($name))
|
||||
{
|
||||
foreach($name as $n) $this->reset($n);
|
||||
return;
|
||||
}
|
||||
if($name === 'class') return $this->class->clear();
|
||||
if($name === 'style') return $this->style->clear();
|
||||
|
||||
$this->remove($name);
|
||||
if($value) $this->setVal($name, $value);
|
||||
}
|
||||
|
||||
public function bindEvent($name, $callback = null)
|
||||
{
|
||||
if(is_array($name))
|
||||
{
|
||||
foreach($name as $key => $value) $this->bindEvent($key, $value);
|
||||
return;
|
||||
}
|
||||
|
||||
$events = parent::getVal("@$name") ?? [];
|
||||
if(is_array($callback)) $events = array_merge($events, $callback);
|
||||
else $events[] = $callback;
|
||||
|
||||
parent::setVal("@$name", $events);
|
||||
}
|
||||
|
||||
public function events(): array
|
||||
{
|
||||
$events = array();
|
||||
foreach($this->data as $name => $value)
|
||||
{
|
||||
if(str_starts_with($name, '@')) $events[substr($name, 1)] = $value;
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function hasEvent(): bool
|
||||
{
|
||||
foreach($this->data as $name => $value)
|
||||
{
|
||||
if(str_starts_with($name, '@')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hx(array|string $name, ?string $value = null)
|
||||
{
|
||||
if(is_array($name))
|
||||
{
|
||||
foreach($name as $key => $val) $this->set("hx-$key", $val);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set("hx-$name", $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert props to html string
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Properties data map:
|
||||
* $map = array(
|
||||
* 'id' => 'sayHelloBtn',
|
||||
* 'data-title' => 'Say "Hello"!',
|
||||
* 'data-content' => null,
|
||||
* 'data-show' => true,
|
||||
* );
|
||||
* // Output string: id="sayHelloBtn" data-title="Say "Hello"!" data-show="true"
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function toStr(array|string $skipProps = array()): string
|
||||
{
|
||||
if(is_string($skipProps)) $skipProps = explode(',', $skipProps);
|
||||
|
||||
$pairs = array();
|
||||
|
||||
if($this->class->count()) $pairs[] = 'class="' . $this->class->toStr() . '"';
|
||||
if($this->style->count(true)) $pairs[] = 'style="' . $this->style->toStr() . '"';
|
||||
|
||||
foreach($this->data as $name => $value)
|
||||
{
|
||||
/* Handle boolean attributes */
|
||||
if(in_array($name, static::$booleanAttrs)) $value = $value ? true : null;
|
||||
|
||||
/* Skip any null value or events setting */
|
||||
if($value === null || in_array($name, $skipProps) || $name[0] === '@') continue;
|
||||
|
||||
/* Convert non-string to json */
|
||||
if($value === true && !str_starts_with($name, 'data-'))
|
||||
{
|
||||
$pairs[] = $name;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!is_string($value)) $value = json_encode($value);
|
||||
|
||||
$pairs[] = $name . '="' . htmlspecialchars($value) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $pairs);
|
||||
}
|
||||
|
||||
public function toJsonData(bool $skipEvents = false): array
|
||||
{
|
||||
$data = $this->data;
|
||||
if(!empty($this->style->data)) $data['style'] = $this->style->data;
|
||||
if(!empty($this->class->list)) $data['class'] = $this->class->toStr();
|
||||
|
||||
if($skipEvents)
|
||||
{
|
||||
foreach($data as $name => $value)
|
||||
{
|
||||
if(str_starts_with($name, '@')) unset($data[$name]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function skip(array|string $skipProps = array(), bool $skipFalse = false): array
|
||||
{
|
||||
if(is_string($skipProps)) $skipProps = explode(',', $skipProps);
|
||||
|
||||
$data = $this->toJsonData();
|
||||
foreach($data as $name => $value)
|
||||
{
|
||||
if($value === null || $name[0] === '@' || in_array($name, $skipProps)) unset($data[$name]);
|
||||
if($skipFalse && $value === false) unset($data[$name]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function split(array|string $firstListProps = array()): array
|
||||
{
|
||||
if(is_string($firstListProps)) $firstListProps = explode(',', $firstListProps);
|
||||
|
||||
$data = $this->toJsonData();
|
||||
$firstList = array();
|
||||
$restList = array();
|
||||
foreach($data as $name => $value)
|
||||
{
|
||||
if($value === null || $name[0] === '@') continue;
|
||||
if(in_array($name, $firstListProps)) $firstList[$name] = $value;
|
||||
else $restList[$name] = $value;
|
||||
}
|
||||
|
||||
return array($firstList, $restList);
|
||||
}
|
||||
|
||||
public function pick(array|string $pickProps = array()): array
|
||||
{
|
||||
if(is_string($pickProps)) $pickProps = explode(',', $pickProps);
|
||||
|
||||
$data = $this->toJsonData();
|
||||
foreach($data as $name => $value)
|
||||
{
|
||||
if($value === null || !in_array($name, $pickProps)) unset($data[$name]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a new instance
|
||||
*
|
||||
* @access public
|
||||
* @return props
|
||||
*/
|
||||
public function clone(): props
|
||||
{
|
||||
$props = new props($this->data);
|
||||
$props->style = clone $this->style;
|
||||
$props->class = clone $this->class;
|
||||
return $props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The rawContent class file of zin lib.
|
||||
*
|
||||
* @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 . 'wg.class.php';
|
||||
|
||||
class rawContent extends wg
|
||||
{
|
||||
protected function build(): directive
|
||||
{
|
||||
return h::comment('{{RAW_CONTENT}}');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The render function file of zin of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author Hao Sun <sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link https://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
require_once __DIR__ . DS . 'zin.class.php';
|
||||
|
||||
/**
|
||||
* 将视图页面声明的所有内容通过一个部件进行渲染,并输出 HTML。
|
||||
* Render page content with a widget to HTML.
|
||||
*
|
||||
* @access public
|
||||
* @param string $wgName
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
function render(string $wgName = '', array $options = array())
|
||||
{
|
||||
/* 获取全局渲染部件实例和指令。 Get global render widgets and directives. */
|
||||
$globalItems = zin::getGlobalRenderList();
|
||||
|
||||
/* 决定部件名称,如果是 Ajax 请求则进行特殊处理。 Decide widget name, if is ajax request, then do special process. */
|
||||
if(empty($wgName))
|
||||
{
|
||||
$wgName = 'page';
|
||||
if(isAjaxRequest('modal')) $wgName = 'modalDialog';
|
||||
else if(isAjaxRequest() && !isAjaxRequest('zin')) $wgName = 'fragment';
|
||||
}
|
||||
|
||||
/* 判断是否渲染为完整页面。 Check if render in full page. */
|
||||
$isFullPage = str_starts_with($wgName, 'page');
|
||||
if($isFullPage) $globalItems[] = set::display(false);
|
||||
|
||||
/* 获取部件渲染选项。 Get widget display options. */
|
||||
if(empty($options) && isset($_SERVER['HTTP_X_ZIN_OPTIONS']) && !empty($_SERVER['HTTP_X_ZIN_OPTIONS']))
|
||||
{
|
||||
$setting = $_SERVER['HTTP_X_ZIN_OPTIONS'];
|
||||
$options = $setting[0] === '{' ? json_decode($setting, true) : array('selector' => $setting);
|
||||
}
|
||||
|
||||
/* 创建部件实例。 Create widget instance. */
|
||||
$wg = createWg($wgName, $globalItems);
|
||||
|
||||
/* 如果不是渲染一个完整页面,则使用 fragment 进行渲染。 If not render in full page, then render all items in a fragment. */
|
||||
if(!$isFullPage && $wgName !== 'fragment') $wg = fragment($wg);
|
||||
|
||||
/* 渲染并输出 HTML。 Render and display html. */
|
||||
$wg->display($options);
|
||||
|
||||
zin::$rendered = true;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The selector helpers 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;
|
||||
|
||||
/**
|
||||
* Parse wg selector
|
||||
* @param string|object $selector
|
||||
* @return object|null
|
||||
*/
|
||||
function parseWgSelector(string|object $selector): ?object
|
||||
{
|
||||
if(is_object($selector)) return $selector;
|
||||
|
||||
$selector = trim($selector);
|
||||
$len = strlen($selector);
|
||||
|
||||
if($len < 1) return null;
|
||||
|
||||
$result = array(
|
||||
'class' => array(),
|
||||
'id' => null,
|
||||
'tag' => null,
|
||||
'inner' => false,
|
||||
'name' => null,
|
||||
'first' => false,
|
||||
'selector' => $selector
|
||||
);
|
||||
if(str_contains($selector, '/'))
|
||||
{
|
||||
$parts = explode('/', $selector, 2);
|
||||
$result['name'] = $parts[0];
|
||||
$selector = $parts[1];
|
||||
$len = strlen($selector);
|
||||
}
|
||||
$selector = str_replace('> *', '>*', $selector);
|
||||
if(substr($selector, strlen($selector) - 2) == '>*')
|
||||
{
|
||||
$result['inner'] = true;
|
||||
$selector = substr($selector, 0, strlen($selector) - 2);
|
||||
$len = strlen($selector);
|
||||
}
|
||||
|
||||
$type = 'tag';
|
||||
$current = '';
|
||||
$updateResult = function(&$result, $current, $type)
|
||||
{
|
||||
if(empty($current)) return;
|
||||
|
||||
if($type === 'class')
|
||||
{
|
||||
$result[$type][] = $current;
|
||||
}
|
||||
elseif($type === 'option')
|
||||
{
|
||||
$options = [];
|
||||
parse_str($current, $options);
|
||||
foreach($options as $key => $value) $result[$key] = empty($value) ? true : $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$result[$type] = $current;
|
||||
}
|
||||
};
|
||||
|
||||
for($i = 0; $i < $len; $i++)
|
||||
{
|
||||
$c = $selector[$i];
|
||||
$t = '';
|
||||
|
||||
if($c === '#' & $type !== 'option')
|
||||
{
|
||||
$t = 'id';
|
||||
}
|
||||
elseif($c === '.' & $type !== 'option')
|
||||
{
|
||||
$t = 'class';
|
||||
}
|
||||
elseif($c === '(' && $type !== 'option' && str_ends_with($selector, ')'))
|
||||
{
|
||||
$command = substr($selector, $i + 1, -1);
|
||||
if(empty($command)) $command = $current;
|
||||
$result['command'] = $command;
|
||||
break;
|
||||
}
|
||||
elseif($c === ':')
|
||||
{
|
||||
$t = 'option';
|
||||
}
|
||||
|
||||
if(empty($t))
|
||||
{
|
||||
$current .= $c;
|
||||
}
|
||||
else
|
||||
{
|
||||
$updateResult($result, $current, $type);
|
||||
$current = '';
|
||||
$type = $t;
|
||||
}
|
||||
}
|
||||
$updateResult($result, $current, $type);
|
||||
|
||||
if(empty($result['class'])) $result['class'] = null;
|
||||
if(empty($result['name']))
|
||||
{
|
||||
if(!empty($result['id'])) $result['name'] = $result['id'];
|
||||
elseif(!empty($result['tag'])) $result['name'] = $result['tag'];
|
||||
else $result['name'] = $selector;
|
||||
}
|
||||
|
||||
return (object)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse wg selectors.
|
||||
* @param object|string|object[]|string[] $selectors
|
||||
* @return object[]
|
||||
*/
|
||||
function parseWgSelectors(object|string|array $selectors): array
|
||||
{
|
||||
if(is_object($selectors)) return array($selectors);
|
||||
if(is_string($selectors)) $selectors = explode(',', trim($selectors));
|
||||
$results = array();
|
||||
foreach($selectors as $selector)
|
||||
{
|
||||
$selector = parseWgSelector($selector);
|
||||
if(is_object($selector)) $results[] = $selector;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify wg selectors.
|
||||
* @param object|object[] $selector
|
||||
* @return string
|
||||
*/
|
||||
function stringifyWgSelectors(array|object $selector): string
|
||||
{
|
||||
if(empty($selector)) return '';
|
||||
if(is_array($selector))
|
||||
{
|
||||
$result = [];
|
||||
foreach($selector as $s) $result[] = stringifyWgSelectors($s);
|
||||
return implode(',', $result);
|
||||
}
|
||||
|
||||
$result = '';
|
||||
if(!empty($selector->name) && $selector->name !== $selector->selector) $result .= $selector->name . '/';
|
||||
if(!empty($selector->tag)) $result .= $selector->tag;
|
||||
if(!empty($selector->id)) $result .= '#' . $selector->id;
|
||||
if(!empty($selector->class)) $result .= '.' . implode('.', $selector->class);
|
||||
if(!empty($selector->first)) $result .= ':first';
|
||||
if($selector->inner) $result .= '>*';
|
||||
return $result;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The properties setter class file of zin lib.
|
||||
*
|
||||
* @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 . 'directive.class.php';
|
||||
|
||||
class set
|
||||
{
|
||||
public static function __callStatic($prop, $args)
|
||||
{
|
||||
$value = array_shift($args);
|
||||
if(is_object($value)) $value = (array)$value;
|
||||
return directive('prop', array($prop => $value));
|
||||
}
|
||||
|
||||
public static function class(...$args)
|
||||
{
|
||||
return directive('prop', array('class' => $args));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The block setter class file of zin lib.
|
||||
*
|
||||
* @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 . 'wg.func.php';
|
||||
|
||||
class to
|
||||
{
|
||||
public static function __callStatic($name, $args)
|
||||
{
|
||||
return to($name, $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,752 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The base widget 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 . 'props.class.php';
|
||||
require_once __DIR__ . DS . 'directive.class.php';
|
||||
require_once __DIR__ . DS . 'zin.class.php';
|
||||
require_once __DIR__ . DS . 'context.class.php';
|
||||
require_once __DIR__ . DS . 'selector.func.php';
|
||||
require_once __DIR__ . DS . 'dom.class.php';
|
||||
|
||||
class wg
|
||||
{
|
||||
/**
|
||||
* Define props for the element
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $defineProps = array();
|
||||
|
||||
protected static array $defaultProps = array();
|
||||
|
||||
protected static array $defineBlocks = array();
|
||||
|
||||
protected static array $wgToBlockMap = array();
|
||||
|
||||
protected static array $definedPropsMap = array();
|
||||
|
||||
private static array $pageResources = array();
|
||||
|
||||
/**
|
||||
* The props of the element
|
||||
*
|
||||
* @access public
|
||||
* @var props
|
||||
*/
|
||||
public props $props;
|
||||
|
||||
public array $blocks = array();
|
||||
|
||||
public ?wg $parent = null;
|
||||
|
||||
public string $gid;
|
||||
|
||||
public bool $displayed = false;
|
||||
|
||||
protected array $renderOptions = array();
|
||||
|
||||
public function __construct(/* string|element|object|array|null ...$args */)
|
||||
{
|
||||
$this->props = new props();
|
||||
|
||||
$this->gid = 'zin_' . uniqid();
|
||||
$this->setDefaultProps(static::getDefaultProps());
|
||||
$this->add(func_get_args());
|
||||
$this->created();
|
||||
|
||||
zin::renderInGlobal($this);
|
||||
static::checkPageResources();
|
||||
|
||||
$this->checkErrors();
|
||||
}
|
||||
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toJsonData();
|
||||
}
|
||||
|
||||
public function isDomElement(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the element is match any of the selectors
|
||||
* @param string|array|object $selectors
|
||||
*/
|
||||
public function isMatch(string|array|object $selectors): bool
|
||||
{
|
||||
$list = parseWgSelectors($selectors);
|
||||
foreach($list as $selector)
|
||||
{
|
||||
if(isset($selector->command)) continue;
|
||||
if(!empty($selector->id) && $this->id() !== $selector->id) continue;
|
||||
if(!empty($selector->tag) && $this->shortType() !== $selector->tag) continue;
|
||||
if(!empty($selector->class) && !$this->props->class->has($selector->class)) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build dom object
|
||||
* @return dom
|
||||
*/
|
||||
public function buildDom(): dom
|
||||
{
|
||||
$before = $this->buildBefore();
|
||||
$children = $this->build();
|
||||
$after = $this->buildAfter();
|
||||
$options = $this->renderOptions;
|
||||
$selectors = (!empty($options) && isset($options['selector'])) ? $options['selector'] : null;
|
||||
|
||||
return new dom
|
||||
(
|
||||
$this,
|
||||
[$before, $children, $after],
|
||||
$selectors,
|
||||
(!empty($options) && isset($options['type'])) ? $options['type'] : 'html', // TODO: () may not work in lower php
|
||||
(!empty($options) && isset($options['data'])) ? $options['data'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget to html
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$dom = $this->buildDom();
|
||||
$result = $dom->render();
|
||||
|
||||
return is_string($result) ? $result : json_encode($result);
|
||||
}
|
||||
|
||||
public function display(array $options = array()): wg
|
||||
{
|
||||
zin::disableGlobalRender();
|
||||
$this->renderOptions = $options;
|
||||
|
||||
$dom = $this->buildDom();
|
||||
$result = $dom->render();
|
||||
$context = context::current();
|
||||
$css = $context->getCSS();
|
||||
$js = $context->getJS();
|
||||
|
||||
global $app, $config;
|
||||
$zinDebug = null;
|
||||
if($config->debug && (!isAjaxRequest() || isAjaxRequest('zin')))
|
||||
{
|
||||
$zinDebug = data('zinDebug');
|
||||
if(is_array($zinDebug))
|
||||
{
|
||||
$zinDebug['basePath'] = $app->getBasePath();
|
||||
if(isset($app->zinErrors)) $zinDebug['errors'] = $app->zinErrors;
|
||||
}
|
||||
}
|
||||
|
||||
$rawContent = ob_get_contents();
|
||||
if(!is_string($rawContent)) $rawContent = '';
|
||||
ob_end_clean();
|
||||
|
||||
if(is_object($result))
|
||||
{
|
||||
if($zinDebug && isset($result['zinDebug'])) $result['zinDebug'] = $zinDebug;
|
||||
$result = json_encode($result);
|
||||
}
|
||||
elseif(is_array($result))
|
||||
{
|
||||
foreach($result as $name => $item)
|
||||
{
|
||||
if(!isset($item['type']) || $item['type'] !== 'html') continue;
|
||||
|
||||
$item['data'] = str_replace('/*{{ZIN_PAGE_CSS}}*/', $css, $item['data']);
|
||||
$item['data'] = str_replace('/*{{ZIN_PAGE_JS}}*/', $js, $item['data']);
|
||||
$item['data'] = str_replace('<!-- {{RAW_CONTENT}} -->', $rawContent, $item['data']);
|
||||
|
||||
$result[$name]['data'] = $item['data'];
|
||||
}
|
||||
if($zinDebug && isset($result['zinDebug'])) $result['zinDebug'] = $zinDebug;
|
||||
$result = json_encode($result);
|
||||
}
|
||||
else
|
||||
{
|
||||
if($zinDebug) $js .= h::createJsVarCode('window.zinDebug', $zinDebug);
|
||||
$result = str_replace('/*{{ZIN_PAGE_CSS}}*/', $css, $result);
|
||||
$result = str_replace('/*{{ZIN_PAGE_JS}}*/', $js, $result);
|
||||
$result = str_replace('<!-- {{RAW_CONTENT}} -->', $rawContent, $result);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
echo $result;
|
||||
|
||||
$this->displayed = true;
|
||||
context::destroy();
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function created() {}
|
||||
|
||||
protected function buildBefore(): array
|
||||
{
|
||||
return $this->block('before');
|
||||
}
|
||||
|
||||
protected function buildAfter(): array
|
||||
{
|
||||
return $this->block('after');
|
||||
}
|
||||
|
||||
protected function build(): array|wg|directive
|
||||
{
|
||||
return $this->children();
|
||||
}
|
||||
|
||||
public function buildEvents(): ?string
|
||||
{
|
||||
$events = $this->props->events();
|
||||
if(empty($events)) return null;
|
||||
|
||||
$id = $this->id();
|
||||
$code = array($this->shortType() === '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.on.zin', 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(" "));';
|
||||
return h::createJsScopeCode($code);
|
||||
}
|
||||
|
||||
|
||||
protected function onAddBlock(array|string|wg|directive $child, string $name)
|
||||
{
|
||||
return $child;
|
||||
}
|
||||
|
||||
protected function onAddChild(array|string|wg|directive $child)
|
||||
{
|
||||
return $child;
|
||||
}
|
||||
|
||||
protected function onSetProp(array|string $prop, mixed $value)
|
||||
{
|
||||
if($prop === 'id' && $value === '$GID') $value = $this->gid;
|
||||
if($prop[0] === '@')
|
||||
{
|
||||
$this->setDefaultProps(array('id' => $this->gid));
|
||||
context::current()->addWgWithEvents($this);
|
||||
}
|
||||
$this->props->set($prop, $value);
|
||||
}
|
||||
|
||||
protected function onGetProp(string $prop, mixed $defaultValue): mixed
|
||||
{
|
||||
return $this->props->get($prop, $defaultValue);
|
||||
}
|
||||
|
||||
public function add($item, string $blockName = 'children')
|
||||
{
|
||||
if($item === null || is_bool($item)) return $this;
|
||||
|
||||
if(is_array($item))
|
||||
{
|
||||
foreach($item as $child) $this->add($child, $blockName);
|
||||
return $this;
|
||||
}
|
||||
|
||||
zin::disableGlobalRender();
|
||||
|
||||
if($item instanceof wg) $this->addToBlock($blockName, $item);
|
||||
elseif(is_string($item)) $this->addToBlock($blockName, htmlentities($item));
|
||||
elseif(isDirective($item)) $this->directive($item, $blockName);
|
||||
else $this->addToBlock($blockName, htmlentities(strval($item)));
|
||||
|
||||
zin::enableGlobalRender();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addToBlock(array|string $name, array|string|null|wg|directive $child = null)
|
||||
{
|
||||
if(is_array($name))
|
||||
{
|
||||
foreach($name as $blockName => $blockChildren)
|
||||
{
|
||||
$this->addToBlock($blockName, $blockChildren);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(is_array($child))
|
||||
{
|
||||
foreach($child as $blockChild)
|
||||
{
|
||||
$this->addToBlock($name, $blockChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if($child instanceof wg && empty($child->parent)) $child->parent = &$this;
|
||||
|
||||
if($name === 'children' && $child instanceof wg)
|
||||
{
|
||||
$blockName = static::getBlockNameForWg($child);
|
||||
if($blockName !== null) $name = $blockName;
|
||||
}
|
||||
|
||||
$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])) $this->blocks[$name][] = $child;
|
||||
else $this->blocks[$name] = array($child);
|
||||
}
|
||||
|
||||
public function children(): array
|
||||
{
|
||||
return $this->block('children');
|
||||
}
|
||||
|
||||
public function block(string $name): array
|
||||
{
|
||||
$list = array();
|
||||
if(isset($this->blocks[$name]))
|
||||
{
|
||||
$blocks = $this->blocks[$name];
|
||||
foreach($blocks as $block)
|
||||
{
|
||||
$isWg = $block instanceof wg && $block->shortType() === 'wg';
|
||||
$block = $isWg ? $block->children() : $block;
|
||||
if(is_array($block)) $list = array_merge($list, $block);
|
||||
else $list[] = $block;
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function hasBlock(string $name): bool
|
||||
{
|
||||
return isset($this->blocks[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply directive
|
||||
*/
|
||||
public function directive(directive &$directive, array|string $blockName)
|
||||
{
|
||||
$data = $directive->data;
|
||||
$type = $directive->type;
|
||||
$directive->parent = &$this;
|
||||
|
||||
if($type === 'prop')
|
||||
{
|
||||
$this->setProp($data);
|
||||
return;
|
||||
}
|
||||
if($type === 'class' || $type === 'style')
|
||||
{
|
||||
$this->setProp($type, $data);
|
||||
return;
|
||||
}
|
||||
if($type === 'cssVar')
|
||||
{
|
||||
$this->setProp('--', $data);
|
||||
return;
|
||||
}
|
||||
if($type === 'html')
|
||||
{
|
||||
$this->addToBlock($blockName, $directive);
|
||||
return;
|
||||
}
|
||||
if($type === 'text')
|
||||
{
|
||||
$this->addToBlock($blockName, htmlspecialchars($data));
|
||||
return;
|
||||
}
|
||||
if($type === 'block')
|
||||
{
|
||||
foreach($data as $blockName => $blockChildren)
|
||||
{
|
||||
$this->add($blockChildren, $blockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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->toJsonData();
|
||||
|
||||
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(array $props)
|
||||
{
|
||||
if(!is_array($props) || empty($props)) return;
|
||||
|
||||
foreach($props as $name => $value)
|
||||
{
|
||||
if($this->props->has($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 type(): string
|
||||
{
|
||||
return get_called_class();
|
||||
}
|
||||
|
||||
public function shortType(): string
|
||||
{
|
||||
$type = $this->type();
|
||||
$pos = strrpos($type, '\\');
|
||||
return $pos === false ? $type : substr($type, $pos + 1);
|
||||
}
|
||||
|
||||
public function id(): ?string
|
||||
{
|
||||
return $this->prop('id');
|
||||
}
|
||||
|
||||
public function toJsonData(): array
|
||||
{
|
||||
$data = array();
|
||||
$data['gid'] = $this->gid;
|
||||
$data['props'] = $this->props->toJsonData();
|
||||
|
||||
$data['type'] = $this->type();
|
||||
if(str_starts_with($data['type'], 'zin\\')) $data['type'] = substr($data['type'], 4);
|
||||
|
||||
$data['blocks'] = array();
|
||||
foreach($this->blocks as $key => $value)
|
||||
{
|
||||
foreach($value as $index => $child)
|
||||
{
|
||||
if($child instanceof wg || (is_object($child) && method_exists($child, 'toJsonData')))
|
||||
{
|
||||
$value[$index] = $child->toJsonData();
|
||||
}
|
||||
elseif(isDirective($child, 'html'))
|
||||
{
|
||||
$value[$index] = $child->data;
|
||||
}
|
||||
}
|
||||
if($key === 'children')
|
||||
{
|
||||
unset($data['blocks'][$key]);
|
||||
$data['children'] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['blocks'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($data['blocks'])) unset($data['blocks']);
|
||||
|
||||
if(!empty($this->parent)) $data['parent'] = $this->parent->gid;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check errors in debug mode.
|
||||
*
|
||||
* @access protected
|
||||
* @return void
|
||||
*/
|
||||
protected function checkErrors()
|
||||
{
|
||||
global $config;
|
||||
if(!isset($config->debug) || !$config->debug) 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;
|
||||
|
||||
trigger_error("[ZIN] The property \"$name: {$definition['type']}\" of widget \"{$this->type()}#$this->gid\" is required.", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$wgErrors = $this->onCheckErrors();
|
||||
if(empty($wgErrors)) return;
|
||||
|
||||
foreach($wgErrors as $error)
|
||||
{
|
||||
if(is_array($error)) trigger_error("[ZIN] $error[0]", count($error) > 1 ? $error[1] : E_USER_WARNING);
|
||||
else trigger_error("[ZIN] $error", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The lifecycle method for checking errors in debug mode.
|
||||
*
|
||||
* @access protected
|
||||
* @return array|null
|
||||
*/
|
||||
protected function onCheckErrors(): array|null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return false; // No css
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return false; // No js
|
||||
}
|
||||
|
||||
protected static function checkPageResources()
|
||||
{
|
||||
$name = get_called_class();
|
||||
if(isset(static::$pageResources[$name])) return;
|
||||
|
||||
static::$pageResources[$name] = true;
|
||||
|
||||
$pageCSS = static::getPageCSS();
|
||||
$pageJS = static::getPageJS();
|
||||
|
||||
if(!empty($pageCSS)) context::css($pageCSS);
|
||||
if(!empty($pageJS)) context::js($pageJS);
|
||||
}
|
||||
|
||||
public static function wgBlockMap(): array
|
||||
{
|
||||
$wgName = get_called_class();
|
||||
if(!isset(wg::$wgToBlockMap[$wgName]))
|
||||
{
|
||||
$wgBlockMap = array();
|
||||
if(!empty(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) $wgBlockMap[$name] = $blockName;
|
||||
}
|
||||
}
|
||||
wg::$wgToBlockMap[$wgName] = $wgBlockMap;
|
||||
}
|
||||
return wg::$wgToBlockMap[$wgName];
|
||||
}
|
||||
|
||||
public static function getBlockNameForWg(wg|string $wg): ?string
|
||||
{
|
||||
$wgType = ($wg instanceof wg) ? $wg->type() : $wg;
|
||||
$wgBlockMap = static::wgBlockMap();
|
||||
if(str_starts_with($wgType, 'zin\\')) $wgType = substr($wgType, 4);
|
||||
return isset($wgBlockMap[$wgType]) ? $wgBlockMap[$wgType] : null;
|
||||
}
|
||||
|
||||
protected static function definedPropsList(?string $wgName = null): array
|
||||
{
|
||||
if($wgName === null) $wgName = get_called_class();
|
||||
|
||||
if(!isset(wg::$definedPropsMap[$wgName]) && $wgName === get_called_class())
|
||||
{
|
||||
wg::$definedPropsMap[$wgName] = static::parsePropsDefinition(static::$defineProps);
|
||||
}
|
||||
return wg::$definedPropsMap[$wgName];
|
||||
}
|
||||
|
||||
protected static function getDefaultProps(?string $wgName = null): array
|
||||
{
|
||||
$defaultProps = array();
|
||||
foreach(static::definedPropsList($wgName) 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|element', 'icon?:string="star"');
|
||||
* $definition = array('name' => 'mixed', 'desc' => 'string', 'title' => array('type' => 'string|element', 'optional' => true), 'icon' => array('type' => 'string', 'default' => 'star', 'optional' => true))))
|
||||
*/
|
||||
private static function parsePropsDefinition(array $definition): array
|
||||
{
|
||||
$parentClass = get_parent_class(get_called_class());
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
$props = $parentClass ? call_user_func("$parentClass::definedPropsList") : array();
|
||||
|
||||
if($parentClass !== false && $definition === $parentClass::$defineProps)
|
||||
{
|
||||
if(!empty(static::$defaultProps) && static::$defaultProps !== $parentClass::$defaultProps)
|
||||
{
|
||||
foreach($props as $name => $value)
|
||||
{
|
||||
if(isset(static::$defaultProps[$name]))
|
||||
{
|
||||
$value['default'] = static::$defaultProps[$name];
|
||||
$props[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $props;
|
||||
}
|
||||
|
||||
foreach($definition as $name => $value)
|
||||
{
|
||||
$optional = false;
|
||||
$type = 'mixed';
|
||||
$default = (isset($props[$name]) && isset($props[$name]['default'])) ? $props[$name]['default'] : null;
|
||||
|
||||
if(is_int($name) && is_string($value))
|
||||
{
|
||||
$value = trim($value);
|
||||
if(!str_contains($value, ':'))
|
||||
{
|
||||
$name = $value;
|
||||
$value = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
list($name, $value) = explode(':', $value, 2);
|
||||
}
|
||||
$name = trim($name);
|
||||
if($name[strlen($name) - 1] === '?')
|
||||
{
|
||||
$name = substr($name, 0, strlen($name) - 1);
|
||||
$optional = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(is_array($value))
|
||||
{
|
||||
$type = isset($value['type']) ? $value['type'] : $type;
|
||||
$default = isset($value['default']) ? $value['default'] : $default;
|
||||
$optional = isset($value['optional'])? $value['optional']: $optional;
|
||||
}
|
||||
else if(is_string($value))
|
||||
{
|
||||
if(!str_contains($value, '='))
|
||||
{
|
||||
$type = $value;
|
||||
$default = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
list($type, $default) = explode('=', $value, 2);
|
||||
}
|
||||
$type = trim($type);
|
||||
|
||||
if(is_string($default)) $default = json_decode(trim($default));
|
||||
}
|
||||
|
||||
$props[$name] = array('type' => empty($type) ? 'mixed' : $type, 'default' => $default, 'optional' => $default !== null || $optional);
|
||||
}
|
||||
|
||||
if(static::$defaultProps && (!$parentClass || static::$defaultProps !== $parentClass::$defaultProps))
|
||||
{
|
||||
foreach(static::$defaultProps as $name => $value)
|
||||
{
|
||||
if(!isset($props[$name])) continue;
|
||||
$props[$name]['default'] = $value;
|
||||
}
|
||||
}
|
||||
return $props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The widget function file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php';
|
||||
require_once __DIR__ . DS . 'props.class.php';
|
||||
require_once __DIR__ . DS . 'directive.class.php';
|
||||
require_once __DIR__ . DS . 'rawcontent.class.php';
|
||||
require_once __DIR__ . DS . 'wg.class.php';
|
||||
require_once __DIR__ . DS . 'context.func.php';
|
||||
|
||||
/**
|
||||
* Create an new widget.
|
||||
*
|
||||
* @return wg
|
||||
*/
|
||||
function wg(): wg
|
||||
{
|
||||
return new wg(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget properties.
|
||||
*
|
||||
* @param string|array|props|null $name
|
||||
* @param mixed $value
|
||||
* @return directive|null
|
||||
*/
|
||||
function set(string|array|props|null $name, mixed $value = null): ?directive
|
||||
{
|
||||
if($name === null) return null;
|
||||
|
||||
$props = null;
|
||||
if($name instanceof props) $props = $name;
|
||||
else if(is_array($name)) $props = $name;
|
||||
else if(is_object($name)) $props = (array)$name;
|
||||
else if(is_string($name)) $props = array($name => $value);
|
||||
return $props ? directive('prop', $props) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget CSS class attribute.
|
||||
*
|
||||
* @param array|string|null ...$classList
|
||||
* @return directive
|
||||
*/
|
||||
function setClass(/* array|string|null ...$classList */): directive
|
||||
{
|
||||
return directive('class', func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget style attribute.
|
||||
*
|
||||
* @return directive
|
||||
*/
|
||||
function setStyle(array|string $name, ?string $value = null): directive
|
||||
{
|
||||
return directive('style', is_array($name) ? $name : array($name => $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget CSS variable.
|
||||
*
|
||||
* @return directive
|
||||
*/
|
||||
function setCssVar(array|string $name, ?string $value = null): directive
|
||||
{
|
||||
return directive('cssVar', is_array($name) ? $name : array($name => $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget ID attribute.
|
||||
*
|
||||
* @return ?directive
|
||||
*/
|
||||
function setID(?string $id = null): directive
|
||||
{
|
||||
return set('id', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget element tag name.
|
||||
*
|
||||
* @return directive
|
||||
*/
|
||||
function setTag(string $id): directive
|
||||
{
|
||||
return set('tagName', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set widget data-* attribute.
|
||||
*
|
||||
* @param string|array $name
|
||||
* @param mixed $value
|
||||
* @return directive
|
||||
*/
|
||||
function setData(string|array $name, mixed $value = null): directive
|
||||
{
|
||||
$map = is_array($name) ? $name : array($name => $value);
|
||||
$attrs = array();
|
||||
foreach($map as $key => $value)
|
||||
{
|
||||
$name = "data-$key";
|
||||
if(is_bool($value)) $attrs[$name] = $value ? 'true' : 'false';
|
||||
else if(is_array($value)) $attrs[$name] = json_encode($value);
|
||||
else $attrs[$name] = $value;
|
||||
}
|
||||
return set($attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener to widget element.
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool|string|array $handler
|
||||
* @param array $options
|
||||
*/
|
||||
function on(string $name, bool|string|array $handler, array|string|bool $options = null): directive
|
||||
{
|
||||
if(is_string($options) && is_string($handler))
|
||||
{
|
||||
$options = array('selector' => $handler, 'handler' => $options);
|
||||
}
|
||||
elseif(is_bool($options))
|
||||
{
|
||||
$options = array('capture' => $options, 'handler' => $handler);
|
||||
}
|
||||
elseif(is_array($options))
|
||||
{
|
||||
$options['handler'] = $handler;
|
||||
}
|
||||
else
|
||||
{
|
||||
$options = array('handler' => $handler);
|
||||
}
|
||||
if(str_contains($name, '__'))
|
||||
{
|
||||
list($name, $flags) = explode('__', $name);
|
||||
if(str_contains($flags, 'capture')) $options['capture'] = true;
|
||||
if(str_contains($flags, 'stop')) $options['stop'] = true;
|
||||
if(str_contains($flags, 'prevent')) $options['prevent'] = true;
|
||||
if(str_contains($flags, 'self')) $options['self'] = true;
|
||||
}
|
||||
return set("@$name", (object)$options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create html content.
|
||||
*
|
||||
* @param string ...$lines
|
||||
* @return directive
|
||||
*/
|
||||
function html(/* string ...$lines */): directive
|
||||
{
|
||||
return directive('html', implode("\n", \zin\utils\flat(func_get_args())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create text content.
|
||||
*
|
||||
* @param string ...$lines
|
||||
* @return directive
|
||||
*/
|
||||
function text(/* string ...$lines */): directive
|
||||
{
|
||||
return directive('text', implode("\n", \zin\utils\flat(func_get_args())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create block content.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed ...$wgs
|
||||
* @return directive
|
||||
*/
|
||||
function to(/* string $name, mixed ...$wgs */): directive
|
||||
{
|
||||
$args = func_get_args();
|
||||
$name = array_shift($args);
|
||||
$wg = new wg(count($args) > 1 ? $args : $args[0]);
|
||||
return directive('block', array($name => $wg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content for block "before".
|
||||
*
|
||||
* @param string $wgs
|
||||
* @return directive
|
||||
*/
|
||||
function before(/* mixed ...$wgs */): directive
|
||||
{
|
||||
return to('before', func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content for block "after".
|
||||
*
|
||||
* @param string $wgs
|
||||
* @return directive
|
||||
*/
|
||||
function after(): directive
|
||||
{
|
||||
return to('after', func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create widget contents inherited from the given widget.
|
||||
*
|
||||
* @param wg|array $item
|
||||
* @return array
|
||||
*/
|
||||
function inherit(wg|array $item): array
|
||||
{
|
||||
if(!($item instanceof wg)) $item = new wg($item);
|
||||
return array(set($item->props), directive('block', $item->blocks), $item->children());
|
||||
}
|
||||
|
||||
/**
|
||||
* Divorce widget from parent.
|
||||
*
|
||||
* @param wg|array $item
|
||||
* @return array
|
||||
*/
|
||||
function divorce(wg|array $item): wg|array
|
||||
{
|
||||
if($item instanceof wg)
|
||||
{
|
||||
$item->parent = null;
|
||||
}
|
||||
else if(is_array($item))
|
||||
{
|
||||
foreach($item as $i) divorce($i);
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given widget list has the given widget type.
|
||||
*
|
||||
* @param wg|array $items
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
function hasWgInList(wg|array $items, string $type): bool
|
||||
{
|
||||
if(!is_array($items)) $items = array($items);
|
||||
foreach($items as $item)
|
||||
{
|
||||
if($item instanceof wg && $item->type() == $type) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group widgets by type.
|
||||
*
|
||||
* @param wg|array $items
|
||||
* @param string $types
|
||||
* @return array
|
||||
*/
|
||||
function groupWgInList(wg|array $items, string|array $types): array
|
||||
{
|
||||
if(is_string($types)) $types = explode(',', $types);
|
||||
$typesMap = array();
|
||||
$restList = array();
|
||||
|
||||
foreach($types as $type) $typesMap[$type] = array();
|
||||
|
||||
foreach($items as $item)
|
||||
{
|
||||
if(!($item instanceof wg)) continue;
|
||||
|
||||
$type = $item->shortType();
|
||||
if(isset($typesMap[$type])) $typesMap[$type][] = $item;
|
||||
else $restList[] = $item;
|
||||
}
|
||||
|
||||
$groups = array();
|
||||
foreach($types as $index => $type) $groups[] = $typesMap[$type];
|
||||
$groups[] = $restList;
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create raw content placeholder.
|
||||
*
|
||||
* @return rawContent
|
||||
*/
|
||||
function rawContent(): rawContent
|
||||
{
|
||||
zin::$rawContentCalled = true;
|
||||
|
||||
return new rawContent();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The zin 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 dirname(__DIR__) . DS . 'utils' . DS . 'deep.func.php';
|
||||
|
||||
class zin
|
||||
{
|
||||
public static array $globalRenderList = array();
|
||||
|
||||
public static bool $enabledGlobalRender = true;
|
||||
|
||||
public static array $data = array();
|
||||
|
||||
public static bool $rendered = false;
|
||||
|
||||
public static bool $rawContentCalled = false;
|
||||
|
||||
public static function getData(string $namePath, mixed $defaultValue = null): mixed
|
||||
{
|
||||
return \zin\utils\deepGet(static::$data, $namePath, $defaultValue);
|
||||
}
|
||||
|
||||
public static function setData(string $namePath, mixed $value)
|
||||
{
|
||||
\zin\utils\deepSet(static::$data, $namePath, $value);
|
||||
}
|
||||
|
||||
public static function enableGlobalRender()
|
||||
{
|
||||
static::$enabledGlobalRender = true;
|
||||
}
|
||||
|
||||
public static function disableGlobalRender()
|
||||
{
|
||||
static::$enabledGlobalRender = false;
|
||||
}
|
||||
|
||||
public static function renderInGlobal(): bool
|
||||
{
|
||||
if(!static::$enabledGlobalRender) return false;
|
||||
|
||||
static::$globalRenderList = array_merge(static::$globalRenderList, func_get_args());
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getGlobalRenderList(bool $clear = true): array
|
||||
{
|
||||
$globalItems = array();
|
||||
|
||||
foreach(static::$globalRenderList as $item)
|
||||
{
|
||||
if(is_object($item))
|
||||
{
|
||||
if((isset($item->parent) && $item->parent) || ($item instanceof wg && $item->shortType() === 'wg'))
|
||||
continue;
|
||||
}
|
||||
$globalItems[] = $item;
|
||||
}
|
||||
|
||||
/* Clear globalRenderList. */
|
||||
if($clear) static::$globalRenderList = array();
|
||||
|
||||
return $globalItems;
|
||||
}
|
||||
}
|
||||
+1691
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The helper methods 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 . 'config.php';
|
||||
|
||||
function setWgVer($ver, $names = null)
|
||||
{
|
||||
global $config;
|
||||
$zinConfig = $config->zin;
|
||||
|
||||
if(is_string($names)) $names = explode(',', $names);
|
||||
if(!is_array($names)) return;
|
||||
|
||||
foreach($names as $name)
|
||||
{
|
||||
$name = trim($name);
|
||||
if(!empty($name)) continue;
|
||||
|
||||
$zinConfig->wgVerMap[$name] = $ver;
|
||||
}
|
||||
}
|
||||
|
||||
function getWgVer($name)
|
||||
{
|
||||
global $config;
|
||||
|
||||
return isset($config->zin->verMap[$name]) ? $config->zin->verMap[$name] : $config->zin->wgVer;
|
||||
}
|
||||
|
||||
function createWg($name, $args): wg
|
||||
{
|
||||
$name = strtolower($name);
|
||||
$wgVer = getWgVer($name);
|
||||
|
||||
include_once __DIR__ . DS . 'wg' . DS . $name . DS . "v$wgVer.php";
|
||||
|
||||
$wgName = "\\zin\\$name";
|
||||
|
||||
return class_exists($wgName) ? (new $wgName($args)) : $wgName($args);
|
||||
}
|
||||
|
||||
if(!function_exists('str_contains'))
|
||||
{
|
||||
/**
|
||||
* Determine if a string contains a given substring
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
function str_contains($haystack, $needle)
|
||||
{
|
||||
return strpos($haystack, $needle) !== false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
function str_contains($haystack, $needle)
|
||||
{
|
||||
return \str_contains($haystack, $needle);
|
||||
}
|
||||
}
|
||||
|
||||
if(!function_exists('str_starts_with'))
|
||||
{
|
||||
/**
|
||||
* Checks if a string starts with a given substring
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
function str_starts_with($haystack, $needle)
|
||||
{
|
||||
return strpos($haystack, $needle) === 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
function str_starts_with($haystack, $needle)
|
||||
{
|
||||
return \str_starts_with($haystack, $needle);
|
||||
}
|
||||
}
|
||||
|
||||
if(!function_exists('str_ends_with'))
|
||||
{
|
||||
/**
|
||||
* Checks if a string starts with a given substring.
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return bool
|
||||
*/
|
||||
function str_ends_with($haystack, $needle)
|
||||
{
|
||||
$length = strlen($needle);
|
||||
if ($length === 0) return true;
|
||||
|
||||
$position = strpos($haystack, $needle);
|
||||
return $position !== false && $position === strlen($haystack) - $length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
function str_ends_with($haystack, $needle)
|
||||
{
|
||||
return \str_ends_with($haystack, $needle);
|
||||
}
|
||||
}
|
||||
|
||||
function uncamelize(string $camelCaps, string $separator = '-'): string
|
||||
{
|
||||
return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
|
||||
}
|
||||
|
||||
function isHTML(string $string): bool
|
||||
{
|
||||
return $string !== strip_tags($string) ? true : false;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The classlist 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\utils;
|
||||
|
||||
/**
|
||||
* Manage classname list for html element and widgets
|
||||
*/
|
||||
class classlist
|
||||
{
|
||||
/**
|
||||
* Store classname list, key => value
|
||||
*
|
||||
* @access public
|
||||
* @var array
|
||||
*/
|
||||
public $list = array();
|
||||
|
||||
/**
|
||||
* Create classname instance
|
||||
*
|
||||
* @access public
|
||||
* @param array ...$list - A string or a class name list
|
||||
*/
|
||||
public function __construct(/* ...$list */)
|
||||
{
|
||||
$list = func_get_args();
|
||||
if(!empty($list)) $this->set($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert classnames to string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toStr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __invoke
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = classlist::create('btn primary');
|
||||
* echo $classlist(); // Output: "btn primary"
|
||||
*
|
||||
* @access public
|
||||
* @param array $list - Class name list
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$list = func_get_args();
|
||||
if(empty($list)) return $this->toStr();
|
||||
return $this->set($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __call to invoke toggle method conveniently
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = classlist::new();
|
||||
*
|
||||
* // Add "primary" class
|
||||
* $classlist->primary();
|
||||
*
|
||||
* // Remove "primary" class
|
||||
* $classlist->primary(false);
|
||||
*
|
||||
* @access public
|
||||
* @return classlist
|
||||
*/
|
||||
public function __call($name, $args)
|
||||
{
|
||||
return $this->toggle($name, !count($args) || $args[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create classname instance
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Set class names
|
||||
* $classlist = new classlist();
|
||||
* $classlist->set('btn primary rounded');
|
||||
*
|
||||
* // Set multiple classnames by string list
|
||||
* $classlist->set(array('btn', 'primary', 'rounded'));
|
||||
*
|
||||
* // Set multiple classnames by a mapped array
|
||||
* $classlist->set(array('btn' => true, 'primary' => true, 'rounded' => $isRounded));
|
||||
*
|
||||
* @access public
|
||||
* @param string|array $list - A string or a class name list
|
||||
* @param bool $reset
|
||||
* @return classlist
|
||||
*/
|
||||
public function set($list, $reset = false)
|
||||
{
|
||||
if(is_string($list)) $list = explode(' ', $list);
|
||||
|
||||
if(is_array($list))
|
||||
{
|
||||
if($reset) $this->list = array();
|
||||
|
||||
$expectedKey = 0;
|
||||
foreach($list as $index => $value)
|
||||
{
|
||||
if(is_array($value))
|
||||
{
|
||||
$this->set($value);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If $index is expected numberic key and the $value is string, then use the $value as the name */
|
||||
if($expectedKey === $index && is_string($value))
|
||||
{
|
||||
$value = trim($value);
|
||||
if(strlen($value) > 0) $this->list[$value] = true;
|
||||
}
|
||||
/* If index is string, then set $index as name */
|
||||
else if(is_string($index))
|
||||
{
|
||||
$index = trim($index);
|
||||
if(strlen($index) === 0) continue;
|
||||
|
||||
$this->list[$index] = boolval($value);
|
||||
}
|
||||
$expectedKey++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add classnames
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = new classlist();
|
||||
* $classlist->add('btn primary rounded');
|
||||
*
|
||||
* // Add multiple classnames by string list
|
||||
* $classlist->add('btn', 'primary', 'rounded');
|
||||
*
|
||||
* @access public
|
||||
* @param array ...$list - classname string joined by space or string array
|
||||
* @return classlist
|
||||
*/
|
||||
public function add(/* ...$list */)
|
||||
{
|
||||
return $this->set(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove classnames
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = new classlist('btn primary rounded');
|
||||
* $classlist->remove('btn primary');
|
||||
*
|
||||
* // Add multiple classnames by string list
|
||||
* $classlist->remove('btn', 'primary');
|
||||
*
|
||||
* @access public
|
||||
* @param array|string $list - classname string joined by space or string array
|
||||
* @return classlist
|
||||
*/
|
||||
public function remove($list)
|
||||
{
|
||||
if(is_string($list)) $list = explode(' ', $list);
|
||||
|
||||
foreach($list as $name)
|
||||
{
|
||||
if(!is_string($name)) continue;
|
||||
$name = trim($name);
|
||||
if(!strlen($name)) continue;
|
||||
|
||||
$this->list[$name] = false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle classname
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = new classlist('btn');
|
||||
* $classlist->toggle('btn'); // class list is ""
|
||||
*
|
||||
* // Toggle class name by flag
|
||||
* $classlist->toggle('primary', true); // class list is "primary"
|
||||
*
|
||||
* @access public
|
||||
* @param string $name - classname string
|
||||
* @return classlist
|
||||
*/
|
||||
public function toggle($name, $toggle = null)
|
||||
{
|
||||
$name = trim($name);
|
||||
if(strlen($name))
|
||||
{
|
||||
if($toggle === null) $toggle = !$this->has($name);
|
||||
$this->list[$name] = $toggle;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether has specific class name
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $classlist = new classlist('btn primary rounded');
|
||||
* echo $classlist->has('btn'); // Output true
|
||||
*
|
||||
* // Check multiple names
|
||||
* echo $classlist->has('btn primary'); // Output true
|
||||
*/
|
||||
public function has($list)
|
||||
{
|
||||
if(is_string($list)) $list = explode(' ', $list);
|
||||
|
||||
foreach($list as $name)
|
||||
{
|
||||
if(!is_string($name)) continue;
|
||||
$name = trim($name);
|
||||
if(!strlen($name)) continue;
|
||||
|
||||
if(!isset($this->list[$name]) || !$this->list[$name]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->list = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert classnames to string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toStr(): string
|
||||
{
|
||||
$names = array();
|
||||
foreach($this->list as $name => $toggle)
|
||||
{
|
||||
if(!$toggle) continue;
|
||||
|
||||
$name = trim($name);
|
||||
if(!strlen($name)) continue;
|
||||
|
||||
$names[] = $name;
|
||||
}
|
||||
return implode(' ', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class names count
|
||||
*
|
||||
* @access public
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an classlist instance
|
||||
*
|
||||
* @param string|array $names - A string or a class name list
|
||||
* @return classlist
|
||||
*/
|
||||
static public function new($names = null)
|
||||
{
|
||||
return (new classlist($names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify class list
|
||||
*
|
||||
* @param string|array $names - A string or a class name list
|
||||
* @return string
|
||||
*/
|
||||
static public function str($names)
|
||||
{
|
||||
return (new classlist($names))->toStr();
|
||||
}
|
||||
|
||||
public function toJSON()
|
||||
{
|
||||
return $this->list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The data 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\utils;
|
||||
|
||||
require_once __DIR__ . DS . 'dataset.class.php';
|
||||
|
||||
/**
|
||||
* Manage data for html element and widgets
|
||||
*/
|
||||
class data extends dataset
|
||||
{
|
||||
public function __constructor()
|
||||
{
|
||||
$list = func_get_args();
|
||||
|
||||
foreach($list as $data) $this->set($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for sub class to modify value on setting it
|
||||
*
|
||||
* @access public
|
||||
* @param array|string $prop - Property name or properties list
|
||||
* @param mixed $value - Property value
|
||||
* @param bool $removeEmpty - Whether to remove empty value
|
||||
* @return dataset
|
||||
*/
|
||||
protected function setVal($prop, $value, $removeEmpty = false)
|
||||
{
|
||||
if($prop[0] === '$') $prop = substr($prop, 1);
|
||||
|
||||
if($value === null || ($removeEmpty && empty($value))) return $this->remove($prop);
|
||||
|
||||
$names = explode('.', $prop);
|
||||
$lastName = array_pop($names);
|
||||
$data = &$this->data;
|
||||
if(!empty($names))
|
||||
{
|
||||
foreach($names as $name)
|
||||
{
|
||||
if(!is_array($data))
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
if(!isset($data[$name])) $data[$name] = array();
|
||||
$data = &$data[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if($value === null || ($removeEmpty && empty($value)))
|
||||
{
|
||||
if(isset($data[$lastName])) unset($data[$lastName]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
$data[$lastName] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getVal($prop)
|
||||
{
|
||||
if($prop[0] === '$') $prop = substr($prop, 1);
|
||||
|
||||
$names = explode('.', $prop);
|
||||
$data = &$this->data;
|
||||
foreach($names as $name)
|
||||
{
|
||||
if(!is_array($data)) return null;
|
||||
$data = &$data[$name];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The dataset 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\utils;
|
||||
|
||||
/**
|
||||
* Manage dataset properties for html element and widgets
|
||||
*/
|
||||
class dataset
|
||||
{
|
||||
/**
|
||||
* Store dataset properties list in an array
|
||||
*
|
||||
* @var array
|
||||
* @access public
|
||||
*/
|
||||
public array $data = array();
|
||||
|
||||
/**
|
||||
* Create an instance, the initialed data can be passed
|
||||
*
|
||||
* @access public
|
||||
* @param array $data - Properties list array
|
||||
*/
|
||||
public function __construct(?array $data = null)
|
||||
{
|
||||
if($data !== null) $this->set($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __set
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @param mixed $value - Property value
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value)
|
||||
{
|
||||
$this->set($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __get
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
$this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __isset
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return $this->has($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __unset
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name)
|
||||
{
|
||||
$this->remove($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dataset to json string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toStr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __invoke
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(string|array $name = null, mixed $value = null): string
|
||||
{
|
||||
if($value !== null || is_array($name)) return $this->set($name, $value);
|
||||
if(is_string($name)) return $this->get($name);
|
||||
|
||||
return $this->toStr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override __call for setting property conveniently
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $dataset = dataset::new();
|
||||
*
|
||||
* // Set color property
|
||||
* $dataset->color('red');
|
||||
*
|
||||
* // Get color property
|
||||
* echo $dataset->color(); // Output "Red"
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
if(count($args)) return $this->set($name, $args[0]);
|
||||
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for sub class to modify value on setting it
|
||||
*
|
||||
* @access protected
|
||||
* @param string $prop - Property name or properties list
|
||||
* @param mixed $value - Property value
|
||||
* @return dataset
|
||||
*/
|
||||
protected function setVal(string $prop, mixed $value): dataset
|
||||
{
|
||||
$this->data[$prop] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getVal(string $prop): mixed
|
||||
{
|
||||
return isset($this->data[$prop]) ? $this->data[$prop] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties count
|
||||
*
|
||||
* @access public
|
||||
* @param bool $skipEmpty - Whether to skip to count empty value
|
||||
* @return int
|
||||
*/
|
||||
public function count($skipEmpty = false): int
|
||||
{
|
||||
if(!$skipEmpty) return count($this->data);
|
||||
|
||||
$count = 0;
|
||||
foreach($this->data as $value)
|
||||
{
|
||||
if($value !== null) $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dataset to json string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toStr(): string
|
||||
{
|
||||
return json_encode($this->toJsonData());
|
||||
}
|
||||
|
||||
public function toJsonData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set property, an array can be passed to set multiple properties
|
||||
*
|
||||
* @access public
|
||||
* @param array|string $prop - Property name or properties list
|
||||
* @param mixed $value - Property value
|
||||
* @return dataset
|
||||
*/
|
||||
public function set(array|string $prop, mixed $value = null): dataset
|
||||
{
|
||||
if(is_array($prop))
|
||||
{
|
||||
foreach($prop as $name => $val) $this->set($name, $val);
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->setVal($prop, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get property value by name
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @param mixed $defaultValue - Optional default value if actual value is null
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($prop, $defaultValue = null)
|
||||
{
|
||||
$val = $this->getVal($prop);
|
||||
return $val === null ? $defaultValue : $val;
|
||||
}
|
||||
|
||||
public function addToList($prop, $values)
|
||||
{
|
||||
if(!is_array($values)) $values = array($values);
|
||||
|
||||
$list = $this->getList($prop);
|
||||
$this->set($prop, array_merge($list, $values));
|
||||
}
|
||||
|
||||
public function getList($prop)
|
||||
{
|
||||
return $this->get($prop, array());
|
||||
}
|
||||
|
||||
public function list($prop, $values = null)
|
||||
{
|
||||
if($values === null) return $this->getList($prop);
|
||||
return $this->setList($prop, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete property by name
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @return dataset
|
||||
*/
|
||||
public function remove($prop)
|
||||
{
|
||||
return $this->setVal($prop, null);
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->data = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether has specified property
|
||||
*
|
||||
* @access public
|
||||
* @param string $prop - Property name
|
||||
* @return boolean
|
||||
*/
|
||||
public function has($prop)
|
||||
{
|
||||
return $this->getVal($prop) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a new instance
|
||||
*
|
||||
* @access public
|
||||
* @return object
|
||||
*/
|
||||
public function clone()
|
||||
{
|
||||
$className = get_called_class();
|
||||
return new $className($this->data);
|
||||
}
|
||||
|
||||
public function merge($data)
|
||||
{
|
||||
if(is_object($data) && isset($data->data)) return $this->set($data->data);
|
||||
return $this->set($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The debug helpers 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\utils;
|
||||
|
||||
$logs = array();
|
||||
|
||||
function log($type, $msg = null, $file)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if(!$config->debug) return;
|
||||
|
||||
if($msg === null)
|
||||
{
|
||||
$msg = $type;
|
||||
$type = 'i';
|
||||
}
|
||||
|
||||
if(is_array($msg))
|
||||
{
|
||||
$msgLines = array();
|
||||
foreach($msg as $m) $msgLines[] = strval($m);
|
||||
$msg = implode(' ', $msgLines);
|
||||
}
|
||||
else
|
||||
{
|
||||
$msg = strval($msg);
|
||||
}
|
||||
|
||||
$logs[] = array(array('type' => strtolower($type), 'msg' => $msg));
|
||||
}
|
||||
|
||||
function logInfo($msg, $file = null) {log('i', $msg, $file);};
|
||||
function logWarn($msg, $file = null) {log('w', $msg, $file);};
|
||||
function logError($msg, $file = null) {log('e', $msg, $file);};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin\utils;
|
||||
|
||||
function deepGet(object|array &$data, string $namePath, mixed $defaultValue = null): mixed
|
||||
{
|
||||
$names = explode('.', $namePath);
|
||||
foreach($names as $name)
|
||||
{
|
||||
if(is_object($data))
|
||||
{
|
||||
if(!isset($data->$name)) return $defaultValue;
|
||||
$data = &$data->$name;
|
||||
continue;
|
||||
}
|
||||
if(!is_array($data) || !isset($data[$name])) return $defaultValue;
|
||||
$data = &$data[$name];
|
||||
}
|
||||
return $data === null ? $defaultValue : $data;
|
||||
}
|
||||
|
||||
function deepSet(array &$data, string $namePath, mixed $value)
|
||||
{
|
||||
$names = explode('.', $namePath);
|
||||
$lastName = array_pop($names);
|
||||
if(!empty($names))
|
||||
{
|
||||
foreach($names as $name)
|
||||
{
|
||||
if(!is_array($data)) return;
|
||||
|
||||
if(!isset($data[$name])) $data[$name] = array();
|
||||
$data = &$data[$name];
|
||||
}
|
||||
}
|
||||
|
||||
$data[$lastName] = $value;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin\utils;
|
||||
|
||||
function flat($array, $prefix = '', $separator = '.')
|
||||
{
|
||||
$result = array();
|
||||
foreach($array as $key => $value)
|
||||
{
|
||||
if(is_array($value))
|
||||
{
|
||||
$result = array_merge($result, flat($value, $prefix . $key . $separator));
|
||||
}
|
||||
else
|
||||
{
|
||||
$result[$prefix . $key] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The hx 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\utils;
|
||||
|
||||
require_once __DIR__ . DS . 'dataset.class.php';
|
||||
|
||||
/**
|
||||
* Manage hx for html element and widgets
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Create a hx object an convert to str string
|
||||
* $hx = hx::new()->boost();
|
||||
*
|
||||
* echo $hx(); // Output 'hx-boost="true"'
|
||||
*
|
||||
* @see https://htmx.org/
|
||||
* @todo @sunhao: Validate hx properties on modifying
|
||||
*/
|
||||
class hx extends dataset
|
||||
{
|
||||
/**
|
||||
* Method for sub class to modify value on setting it
|
||||
*
|
||||
* @access public
|
||||
* @param array|string $prop - Property name or properties list
|
||||
* @param mixed $value - Property value
|
||||
* @param bool $removeEmpty - Whether to remove empty value
|
||||
* @return hx
|
||||
*/
|
||||
protected function setVal($prop, $value, $removeEmpty = false)
|
||||
{
|
||||
if(str_starts_with($prop, 'hx-')) $prop = substr($prop, 3);
|
||||
return parent::setVal($prop, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ajax request
|
||||
*
|
||||
* @access public
|
||||
* @param string $url - The request url
|
||||
* @param string $trigger - The trigget
|
||||
* @param string $target - A css selector to specific a element to load remote content
|
||||
* @param string $method - The request method, default value is "get"
|
||||
* @return hx
|
||||
* @see https://htmx.org/docs/#ajax
|
||||
*/
|
||||
public function ajax($url, $trigger = '', $target = '', $method = 'get')
|
||||
{
|
||||
if(is_array($url)) return $this->set($url);
|
||||
|
||||
return $this->set(array('url' => $url, 'trigger' => $trigger, 'target' => $target, 'method' => $method));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ajax post request
|
||||
*
|
||||
* @access public
|
||||
* @param string $url - The request url
|
||||
* @param string $trigger - The trigget
|
||||
* @param string $target - A css selector to specific a element to load remote content
|
||||
* @return hx
|
||||
* @see https://htmx.org/docs/#ajax
|
||||
*/
|
||||
public function post($url, $trigger = '', $target = '')
|
||||
{
|
||||
return $this->ajax($url, $trigger, $target, 'post');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hx properties to str string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toStr()
|
||||
{
|
||||
$pairs = array();
|
||||
|
||||
foreach($this->data as $name => $value)
|
||||
{
|
||||
/* Skip any null value */
|
||||
if($value === null) continue;
|
||||
|
||||
/* Convert non-string to json */
|
||||
if(!is_string($value)) $value = json_encode($value);
|
||||
|
||||
$pairs[] = 'hx-' . $name . '="' . htmlspecialchars($value) . '"';
|
||||
}
|
||||
|
||||
return implode(' ', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
*
|
||||
* @param string $hx - CSS hx list
|
||||
* @return hx
|
||||
*/
|
||||
static public function new($hx)
|
||||
{
|
||||
return new hx($hx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create properties string from hx list
|
||||
*
|
||||
* @access public
|
||||
* @param string $hx - CSS hx list
|
||||
* @return string
|
||||
*/
|
||||
static public function str($props)
|
||||
{
|
||||
return (new hx($props))->toStr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The style 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\utils;
|
||||
|
||||
require_once __DIR__ . DS . 'dataset.class.php';
|
||||
|
||||
/**
|
||||
* Manage style for html element and widgets
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Create a style object an convert to css string
|
||||
* $style = style::create(array('color' => 'red'));
|
||||
* echo $style(); // Output "color:red"
|
||||
*
|
||||
* // Above example same as:
|
||||
* echo style::css(array('color' => 'red'));
|
||||
*
|
||||
* // Modifier style
|
||||
* $style = style::create(array('color' => 'red'));
|
||||
* $style->set('background', 'green');
|
||||
*
|
||||
* // Modifier style with property name directly
|
||||
* $style->background = 'green';
|
||||
*
|
||||
* // Get style value
|
||||
* echo $style->get('background'); // Output "green"
|
||||
*
|
||||
* // Get style value with property name directly
|
||||
* echo $style->background; // Output "green"
|
||||
*
|
||||
* @todo @sunhao: Validate style properties on modifying
|
||||
*/
|
||||
class style extends dataset
|
||||
{
|
||||
/**
|
||||
* Set or get css variable, an array can be passed to set multiple variables
|
||||
* If only pass variable name, then the variable value will be returned
|
||||
* If no params passed, then return all setted variables with an array
|
||||
*
|
||||
* Notice: no need to prepend prefix '--' to variable name, the method will prepend it automatically, if prepended already, the method will skip to prepend smartly
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Create a style object and set
|
||||
* $style = new style();
|
||||
* $style->cssVar('text-size', '14px');
|
||||
*
|
||||
* // Set multiple variables
|
||||
* $style->cssVar(array('text-color' => 'yellow', 'background-image' => 'none'));
|
||||
*
|
||||
* // Get variable value
|
||||
* echo $style->cssVar('text-size'); // Output "14px"
|
||||
*
|
||||
* // Get all variables value
|
||||
* echo $style->cssVar();
|
||||
* // Output array('text-size' => '14px', 'color' => 'yellow', 'background' => 'none');
|
||||
*
|
||||
* // Remove variable by setting value with an empty string
|
||||
* $style->cssVar('text-color', '');
|
||||
*
|
||||
* @access public
|
||||
* @param array|string $name - Variable name or variables list
|
||||
* @param mixed $value - Property value
|
||||
* @return mixed
|
||||
*/
|
||||
public function cssVar(array|string $name = '', ?string $value = null): style|array|string
|
||||
{
|
||||
/* Support for setting multiple variables by an array */
|
||||
if(is_array($name))
|
||||
{
|
||||
foreach($name as $n => $value) $this->set(style::formatVarName($n), $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* Return all setted variables without passed any params */
|
||||
if(empty($name))
|
||||
{
|
||||
$vars = array();
|
||||
foreach ($this->data as $prop => $value)
|
||||
{
|
||||
if(!str_starts_with($name, '--')) continue;
|
||||
$vars[substr($prop, 2)] = $value;
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
|
||||
$varName = style::formatVarName($name);
|
||||
|
||||
/* Return the specific variable value by name */
|
||||
if($value === null) return $this->get($varName);
|
||||
|
||||
/* Set the specific variable value and return style object self */
|
||||
$this->set($varName, $value === '' ? null : $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toStr(): string
|
||||
{
|
||||
return $this->toCss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert style to css string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toCss(): string
|
||||
{
|
||||
$pairs = array();
|
||||
|
||||
foreach($this->data as $prop => $value)
|
||||
{
|
||||
/* Skip any empty value */
|
||||
if($value === null || $value === '') continue;
|
||||
|
||||
$pairs[] = $prop . ': ' . strval($value) . ';';
|
||||
}
|
||||
|
||||
return implode(' ', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
*
|
||||
* @param ?array $style - CSS style list
|
||||
* @return style
|
||||
*/
|
||||
public static function new(?array $style = null): style
|
||||
{
|
||||
return new style($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create css from style list
|
||||
*
|
||||
* @access public
|
||||
* @param ?array $style - CSS style list
|
||||
* @return string
|
||||
*/
|
||||
public static function css(?array $style): string
|
||||
{
|
||||
return (new style($style))->toCss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format CSS variable name with prefix "--"
|
||||
*
|
||||
* @access public
|
||||
* @param string $name - CSS variable name
|
||||
* @return string
|
||||
*/
|
||||
public static function formatVarName(string $name): string
|
||||
{
|
||||
return \zin\str_starts_with($name, '--') ? $name : "--$name";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'dropdown' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'checkbox' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'btngroup' . DS . 'v1.php';
|
||||
|
||||
class actionItem extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'name:string="action"',
|
||||
'type:string="item"',
|
||||
'outerTag:string="li"',
|
||||
'tagName:string="a"',
|
||||
'icon?:string',
|
||||
'text?:string',
|
||||
'textClass?: string',
|
||||
'url?:string',
|
||||
'target?:string',
|
||||
'active?:bool',
|
||||
'disabled?:bool',
|
||||
'trailingIcon?:string',
|
||||
'outerProps?:array',
|
||||
'outerClass?:string',
|
||||
'badge?:string|array|object',
|
||||
'props?:array',
|
||||
'dropdown?:array',
|
||||
'items?:array',
|
||||
'caret?:bool|string'
|
||||
);
|
||||
|
||||
protected function buildDividerItem()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function buildHeadingItem()
|
||||
{
|
||||
list($icon, $text, $trailingIcon, $textClass) = $this->prop(array('icon', 'text', 'trailingIcon', 'textClass'));
|
||||
|
||||
return h::div
|
||||
(
|
||||
set($this->props->skip(array_keys(actionItem::definedPropsList()))),
|
||||
set($this->prop('props')),
|
||||
$icon ? icon($icon) : null,
|
||||
empty($text) ? null : span($text, setClass('text', $textClass)),
|
||||
$this->children(),
|
||||
$trailingIcon ? icon($trailingIcon) : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildDropdownItem()
|
||||
{
|
||||
list($dropdown, $items, $icon, $text, $trailingIcon, $active, $disabled, $badge, $props, $caret, $textClass) = $this->prop(array('dropdown', 'items', 'icon', 'text', 'trailingIcon', 'active', 'disabled', 'badge', 'props', 'caret', 'textClass'));
|
||||
|
||||
if(is_string($badge))
|
||||
{
|
||||
$badge = label($badge);
|
||||
}
|
||||
elseif(is_array($badge))
|
||||
{
|
||||
$badge = label(set($badge));
|
||||
}
|
||||
|
||||
$dropdown = new dropdown
|
||||
(
|
||||
set::items($items),
|
||||
set($dropdown),
|
||||
h::a(
|
||||
setClass(array('active' => $active, 'disabled' => $disabled)),
|
||||
set($this->getRestProps()),
|
||||
set($props),
|
||||
$icon ? icon($icon) : null,
|
||||
span($text, setClass('text', $textClass)),
|
||||
$badge,
|
||||
$this->children(),
|
||||
$trailingIcon ? icon($trailingIcon) : null,
|
||||
h::span(setClass(is_string($caret) ? "caret-$caret" : 'caret'))
|
||||
)
|
||||
);
|
||||
return $dropdown;
|
||||
}
|
||||
|
||||
protected function buildBtnItem()
|
||||
{
|
||||
return new btn($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children());
|
||||
}
|
||||
|
||||
protected function buildCheckboxItem()
|
||||
{
|
||||
return new checkbox($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children());
|
||||
}
|
||||
|
||||
protected function buildBtnGroupItem()
|
||||
{
|
||||
return new btnGroup($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children());
|
||||
}
|
||||
|
||||
protected function buildItem()
|
||||
{
|
||||
$type = $this->prop('type');
|
||||
$methodName = "build{$type}Item";
|
||||
if(method_exists($this, $methodName)) return $this->$methodName();
|
||||
|
||||
list($tagName, $icon, $text, $trailingIcon, $url, $target, $active, $disabled, $badge, $textClass) = $this->prop(array('tagName', 'icon', 'text', 'trailingIcon', 'url', 'target', 'active', 'disabled', 'badge', 'textClass'));
|
||||
|
||||
if(is_string($badge)) $badge = label($badge);
|
||||
else if(is_array($badge)) $badge = label(set($badge));
|
||||
|
||||
return h::create
|
||||
(
|
||||
$tagName,
|
||||
set($tagName === 'a' ? array('href' => $url, 'target' => $target) : array('data-url' => $url, 'data-target' => $target)),
|
||||
setClass(array('active' => $active, 'disabled' => $disabled)),
|
||||
set($this->getRestProps()),
|
||||
set($this->prop('props')),
|
||||
$icon ? icon($icon) : null,
|
||||
span($text, setClass('text', $textClass)),
|
||||
$badge,
|
||||
$this->children(),
|
||||
$trailingIcon ? icon($trailingIcon) : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
list($name, $type, $outerTag, $outerProps, $outerClass) = $this->prop(array('name', 'type', 'outerTag', 'outerProps', 'outerClass'));
|
||||
|
||||
return h::create
|
||||
(
|
||||
$outerTag,
|
||||
setClass(($type !== 'item' && $type !== 'divider') ? 'nav-item' : '', "$name-$type", $outerClass),
|
||||
set($outerProps),
|
||||
$this->buildItem()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class avatar extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'className?:string',
|
||||
'style?:array',
|
||||
'size?:int=32',
|
||||
'circle?:bool=true',
|
||||
'rounded?:string|int',
|
||||
'background?:string',
|
||||
'foreColor?:string',
|
||||
'text?:string',
|
||||
'code?:string',
|
||||
'maxTextLength?:int=2',
|
||||
'hueDistance?:int=43',
|
||||
'saturation?:int=0.4',
|
||||
'lightness?:int=0.6',
|
||||
'src?:string'
|
||||
);
|
||||
|
||||
private $textLen = 0;
|
||||
private $displayTextLen = 0;
|
||||
private $sizeMap = array('xs' => 20, 'sm' => 24, 'lg' => 48, 'xl' => 80);
|
||||
private $actualSize = 32;
|
||||
private $finalClass = array('avatar');
|
||||
private $finalStyle;
|
||||
|
||||
protected function onAddChild($child)
|
||||
{
|
||||
if(is_string($child) && !$this->props->has('text'))
|
||||
{
|
||||
$this->setProp('text', $child);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $child;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
/* Attach classes. */
|
||||
$this->finalClass[] = $this->prop('className');
|
||||
|
||||
/* Init style. */
|
||||
$this->finalStyle = new stdClass();
|
||||
$this->finalStyle->background = $this->prop('background');
|
||||
$this->finalStyle->color = $this->prop('foreColor');
|
||||
|
||||
foreach($this->props->style->data as $attr => $val) $this->finalStyle->{$attr} = $val;
|
||||
|
||||
/* Init avatar size. */
|
||||
$this->initSize();
|
||||
/* Init avatar shape. */
|
||||
$this->initShape();
|
||||
|
||||
$content = $this->getContent();
|
||||
$finalStyle = json_decode(json_encode($this->finalStyle), true);
|
||||
return h::div
|
||||
(
|
||||
setClass($this->finalClass),
|
||||
setStyle($finalStyle),
|
||||
set($this->getRestProps()),
|
||||
$content,
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
|
||||
private function initSize()
|
||||
{
|
||||
$size = $this->prop('size');
|
||||
$this->actualSize = $size;
|
||||
|
||||
if(!$size) return;
|
||||
|
||||
if(is_numeric($size))
|
||||
{
|
||||
$fontSize = intval($size/2) > 12 ? intval($size/2) : 12;
|
||||
$this->finalStyle->width = "{$size}px";
|
||||
$this->finalStyle->height = "{$size}px";
|
||||
$this->finalStyle->{'font-size'} = "{$fontSize}px";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->finalClass[] = "size-{$size}";
|
||||
$this->actualSize = isset($this->sizeMap[$size]) ? $this->sizeMap[$size] : 20;
|
||||
}
|
||||
|
||||
private function initShape()
|
||||
{
|
||||
$circle = $this->prop('circle');
|
||||
$rounded = $this->prop('rounded');
|
||||
|
||||
/* Set circle. */
|
||||
if($circle)
|
||||
{
|
||||
$this->finalClass[] = 'rounded-full';
|
||||
}
|
||||
else if($rounded)
|
||||
{
|
||||
if(is_numeric($rounded)) $this->finalStyle->{'border-radius'} = "{$rounded}px";
|
||||
else $this->finalClass[] = "rounded-{$rounded}";
|
||||
}
|
||||
}
|
||||
|
||||
private function getAvatarText()
|
||||
{
|
||||
$maxTextLen = intval($this->prop('maxTextLength'));
|
||||
$text = strtoupper($this->prop('text', ''));
|
||||
$this->textLen = strlen($text);
|
||||
|
||||
if(preg_match('/[\x{4e00}-\x{9fa5}\s]+$/u', $text))
|
||||
{
|
||||
$this->textLen = mb_strlen($text);
|
||||
$text = $this->textLen <= $maxTextLen ? $text : mb_substr($text, $this->textLen - $maxTextLen);
|
||||
$this->displayTextLen = mb_strlen($text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
if(preg_match('/[A-Za-z\d\s]+$/', $text))
|
||||
{
|
||||
$this->displayTextLen = 1;
|
||||
return substr($text, 0, 1);
|
||||
}
|
||||
|
||||
return $this->textLen <= $maxTextLen ? $text : substr($text, 0, $maxTextLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HSL values to RGB value.
|
||||
*
|
||||
* @param int $h
|
||||
* @param int $s
|
||||
* @param int $l
|
||||
* @access private
|
||||
* @return array
|
||||
*/
|
||||
private function hslToRgb($h, $s, $l)
|
||||
{
|
||||
$h = ($h % 360) / 360;
|
||||
$s = ($s > 0 ? $s : 0);
|
||||
$s = ($s > 255) ? 255 : $s;
|
||||
$l = ($l > 0 ? $l : 0);
|
||||
$l = ($l > 255) ? 255 : $l;
|
||||
|
||||
$m2 = ($l <= 0.5) ? ($l * ($s + 1)) : ($l + $s - $l * $s);
|
||||
$m1 = $l * 2 - $m2;
|
||||
|
||||
$hueFn = function($val, $m1, $m2)
|
||||
{
|
||||
$val = $val < 0 ? $val + 1 : ($val > 1 ? $val - 1 : $val);
|
||||
|
||||
if($val * 6 < 1) return $m1 + ($m2 - $m1) * $val * 6;
|
||||
elseif($val * 2 < 1) return $m2;
|
||||
elseif($val * 3 < 2) return $m1 + ($m2 - $m1) * (2/3 - $val) * 6;
|
||||
|
||||
return $m1;
|
||||
};
|
||||
|
||||
return array(
|
||||
'r' => $hueFn($h + 1/3, $m1, $m2) * 255,
|
||||
'g' => $hueFn($h, $m1, $m2) * 255,
|
||||
'b' => $hueFn($h - 1/3, $m1, $m2) * 255
|
||||
);
|
||||
}
|
||||
|
||||
private function hex2Rgb($hex)
|
||||
{
|
||||
if(!str_starts_with($hex, '#') || !preg_match('/#[0-9A-F]{3,6}$/', $hex)) throw new \Exception('incorrect data format');
|
||||
|
||||
$r = 0;
|
||||
$g = 0;
|
||||
$b = 0;
|
||||
if(strlen($hex) == 4) list($r, $g, $b) = sscanf($hex, "#%01x%01x%01x");
|
||||
elseif(strlen($hex) == 7) list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x");
|
||||
else throw new \Exception('incorrect RGB value');
|
||||
|
||||
return array(
|
||||
'r' => $r,
|
||||
'g' => $g,
|
||||
'b' => $b
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get contrast color.
|
||||
*
|
||||
* @param array|string $rgb
|
||||
* @param string $theme dark|light
|
||||
* @access private
|
||||
* @return string
|
||||
*/
|
||||
private function contrastColor($rgb, $themeDark = null, $themeLight = null)
|
||||
{
|
||||
$rgb = is_array($rgb) ? $rgb : $this->hex2Rgb($rgb);
|
||||
|
||||
$r = $rgb['r'];
|
||||
$g = $rgb['g'];
|
||||
$b = $rgb['b'];
|
||||
if(($r * 0.299 + $g * 0.587 + $b * 0.114) > 186)
|
||||
{
|
||||
/* Is light color. */
|
||||
return $themeDark ? $themeDark : '#333333';
|
||||
}
|
||||
|
||||
return $themeLight ? $themeLight : '#ffffff';
|
||||
}
|
||||
|
||||
private function getTextStyle()
|
||||
{
|
||||
$hueDistance = intval($this->prop('hueDistance'));
|
||||
$saturation = $this->prop('saturation');
|
||||
$lightness = $this->prop('lightness');
|
||||
$background = $this->prop('background');
|
||||
$foreColor = $this->prop('foreColor');
|
||||
$code = $this->prop('code');
|
||||
$avatarCode = $code ? $code : $this->prop('text');
|
||||
|
||||
if(!$background)
|
||||
{
|
||||
$val = 0;
|
||||
if(is_numeric($avatarCode))
|
||||
{
|
||||
$val = intval($avatarCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
for($i = 0; $i < strlen($avatarCode); $i++) $val += ord($avatarCode[$i]);
|
||||
}
|
||||
|
||||
$hue = $val * $hueDistance % 360;
|
||||
$actualSat = $saturation * 100;
|
||||
$actualLight = $lightness * 100;
|
||||
$this->finalStyle->background = "hsl({$hue}, {$actualSat}%, {$actualLight}%)";
|
||||
|
||||
if(!$foreColor)
|
||||
{
|
||||
$rgb = $this->hslToRgb($hue, $saturation, $lightness);
|
||||
$this->finalStyle->color = $this->contrastColor($rgb);
|
||||
}
|
||||
}
|
||||
elseif (!$foreColor && $background)
|
||||
{
|
||||
$this->finalStyle->color = $this->contrastColor($background);
|
||||
}
|
||||
|
||||
$textStyle = array();
|
||||
if($this->actualSize and $this->actualSize < (14 * $this->displayTextLen))
|
||||
{
|
||||
$textStyle = array(
|
||||
'transform' => 'scale(' . $this->actualSize / (14 * $this->displayTextLen) . ')',
|
||||
'white-space' => 'nowrap'
|
||||
);
|
||||
}
|
||||
|
||||
return $textStyle;
|
||||
}
|
||||
|
||||
private function getContent()
|
||||
{
|
||||
$src = $this->prop('src');
|
||||
$text = $this->prop('text');
|
||||
|
||||
/* With avatar. */
|
||||
if($src)
|
||||
{
|
||||
$this->finalClass[] = 'has-img';
|
||||
|
||||
return h::img
|
||||
(
|
||||
setClass('avatar-img'),
|
||||
set('src', $src ),
|
||||
set('alt', $text)
|
||||
);
|
||||
}
|
||||
|
||||
/* Without text and image. */
|
||||
if(!$text) return null;
|
||||
|
||||
$displayText = $this->getAvatarText();
|
||||
|
||||
$this->finalClass[] = 'has-text';
|
||||
$this->finalClass[] = 'has-text-' . $this->textLen;
|
||||
|
||||
$textStyle = $this->getTextStyle();
|
||||
return h::div
|
||||
(
|
||||
setClass('avatar-text'),
|
||||
set('data-actualSize', $this->actualSize),
|
||||
$textStyle ? setStyle($textStyle) : null,
|
||||
$displayText
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The backBtn widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 后退按钮(backBtn)部件类。
|
||||
* The back button widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class backBtn extends btn
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'back?: string="APP"' // 定义返回行为,可以为 `'APP'`(默认值,返回打开当前页面时的上一个历史记录)、 `'GLOBAL'`(返回上一个全局历史记录)、`'moduleName-methodName'`(从历史记录中向后查找符合指定路径的历史记录)。
|
||||
);
|
||||
|
||||
/**
|
||||
* Override the getProps method.
|
||||
*
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function getProps(): array
|
||||
{
|
||||
global $app;
|
||||
|
||||
$backs = array(
|
||||
'task' => 'execution-task,my-work,my-contribute,',
|
||||
'story' => 'product-browse,projectstory-story,execution-story,my-work,my-contribute,productplan-view',
|
||||
'bug' => 'bug-browse,project-bug,my-work,my-contribute,',
|
||||
'testcase' => 'testcase-browse,project-testcase,my-work,my-contribute,',
|
||||
'testsuite' => 'testsuite-browse,testsuite-view,',
|
||||
'testtask' => 'testtask-browse,testtask-cases,',
|
||||
'testreport' => 'testreport-browse,project-testreport',
|
||||
'tree' => 'product-browse,project-browse,execution-task,bug-browse,projectstory-story',
|
||||
'doc' => 'doc-mySpace,doc-productSpace,doc-projectSpace,doc-teamSpace',
|
||||
'design' => 'design-browse',
|
||||
'release' => 'release-browse,release-view',
|
||||
'projectrelease' => 'projectrelease-browse',
|
||||
'build' => 'execution-build,build-view',
|
||||
'projectbuild' => 'projectbuild-browse,projectbuild-view',
|
||||
'mr' => 'mr-browse',
|
||||
'repo' => 'repo-log,repo-browse',
|
||||
'compile' => 'compile-browse',
|
||||
);
|
||||
|
||||
$props = parent::getProps();
|
||||
$back = $this->prop('back');
|
||||
if($back != 'APP')
|
||||
{
|
||||
$props['data-back'] = $back;
|
||||
}
|
||||
elseif(isset($backs[$app->rawModule]))
|
||||
{
|
||||
$props['data-back'] = $backs[$app->rawModule];
|
||||
|
||||
if(!$this->prop('url'))
|
||||
{
|
||||
$backLinks = explode(',', $backs[$app->rawModule]);
|
||||
$props['data-url'] = $backLinks[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$props['data-back'] = empty($back) ? 'APP' : $back;
|
||||
}
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the getClassList method.
|
||||
*
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function getClassList(): array
|
||||
{
|
||||
$classList = parent::getClassList();
|
||||
$classList['open-url'] = true;
|
||||
return $classList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Add new item.
|
||||
*
|
||||
* @param obj e
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
function addItem(e)
|
||||
{
|
||||
const obj = e.target
|
||||
const newItem = $(obj).closest('.form-row').clone();
|
||||
let index = 0;
|
||||
|
||||
newItem.find('.add-btn').on('click', addItem);
|
||||
newItem.find('.del-btn').on('click', removeItem);
|
||||
|
||||
let inputName = newItem.find('input').length > 0 ? newItem.find('input').first().attr('name') : newItem.find('select').first().attr('name');
|
||||
inputName = inputName.slice(0, inputName.indexOf('['));
|
||||
$('form').find("[name^='" + inputName + "']").each(function() {
|
||||
let $name = $(this).attr('name');
|
||||
|
||||
let id = parseInt($name.slice($name.indexOf('[')+1, $name.indexOf(']')));
|
||||
if(isNumeric(id) && id >= index) index = id + 1;
|
||||
})
|
||||
|
||||
/* Fix id and value. */
|
||||
newItem.addClass('newItem');
|
||||
newItem.find('.form-label').html('');
|
||||
newItem.find('input').each(function()
|
||||
{
|
||||
let name = $(this).attr('name');
|
||||
name = name.slice(0, name.indexOf('[')+1) + String(index) + name.slice(name.indexOf(']'));
|
||||
$(this).attr('name', name);
|
||||
$(this).attr('id', name);
|
||||
$(this).val('');
|
||||
});
|
||||
newItem.find('select').each(function()
|
||||
{
|
||||
let name = $(this).attr('name');
|
||||
name = name.slice(0, name.indexOf('[')+1) + String(index) + name.slice(name.indexOf(']'));
|
||||
$(this).attr('name', name);
|
||||
$(this).attr('id', name);
|
||||
$(this).val('');
|
||||
});
|
||||
|
||||
$(obj).closest('.form-row').after(newItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item.
|
||||
*
|
||||
* @param obj e
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
function removeItem(e)
|
||||
{
|
||||
const obj = e.target
|
||||
|
||||
/* Dsiabled btn can't remove line. */
|
||||
if($(obj).closest('.btn').hasClass('disabled')) return false;
|
||||
|
||||
$(obj).closest('.form-row').remove();
|
||||
|
||||
let chosenProducts = 0;
|
||||
$("select[name^='products']").each(function()
|
||||
{
|
||||
if($(this).val() > 0) chosenProducts ++;
|
||||
});
|
||||
|
||||
(chosenProducts.length > 1 && (model == 'waterfall' || model == 'waterfallplus')) ? $('.stageBy').removeClass('hide') : $('.stageBy').addClass('hide');
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class batchActions extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'actionClass?: string=""',
|
||||
);
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
return formGroup
|
||||
(
|
||||
setClass('ml-2'),
|
||||
div
|
||||
(
|
||||
setClass($this->prop('actionClass')),
|
||||
btn
|
||||
(
|
||||
icon('plus', set::size('lg')),
|
||||
setClass('bg-white ring-0 rounded bg-opacity-20 add-btn'),
|
||||
on::click('addItem'),
|
||||
),
|
||||
btn
|
||||
(
|
||||
icon('close', set::size('lg')),
|
||||
setClass('bg-white ring-0 rounded bg-opacity-20 del-btn'),
|
||||
on::click('removeItem'),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The blockPanel widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'panel' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 仪表盘区块面板(blockPanel)部件类。
|
||||
* The block panel widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class blockPanel extends panel
|
||||
{
|
||||
protected static array $defineProps = array
|
||||
(
|
||||
'class?: string="rounded bg-canvas panel-block"', // 类名。
|
||||
'id?: string', // ID。
|
||||
'name?: string', // 区块内部名称。
|
||||
'block?: object|array', // 区块对象。
|
||||
'title?: string', // 标题。
|
||||
'headingClass?: string="border-b"', // 标题栏类名。
|
||||
'moreLink?: string' // 更多链接。
|
||||
);
|
||||
|
||||
protected function created()
|
||||
{
|
||||
global $lang;
|
||||
$props = array();
|
||||
|
||||
$name = $this->prop('name');
|
||||
$block = $this->prop('block', data('block'));
|
||||
|
||||
if(is_array($block)) $block = (object)$block;
|
||||
if(empty($name) && !empty($block))
|
||||
{
|
||||
$name = $block->code;
|
||||
$props['name'] = $name;
|
||||
|
||||
if(empty($this->prop('id'))) $props['id'] = $block->module . '-' . $block->code . '-' . $block->id;
|
||||
}
|
||||
|
||||
$moreLink = $this->prop('moreLink');
|
||||
if(empty($moreLink) && !empty($block) && isset($block->moreLink)) $moreLink = $block->moreLink;
|
||||
if(empty($this->prop('headingActions')) && !empty($moreLink))
|
||||
{
|
||||
$props['headingActions'] = array(array('type' => 'ghost', 'url' => $moreLink, 'text' => $lang->more, 'caret' => 'right', 'size' => 'sm'));
|
||||
}
|
||||
|
||||
if(empty($this->prop('title'))) $props['title'] = empty($block) ? $lang->block->titleList[$name] : $block->title;
|
||||
|
||||
$this->setProp($props);
|
||||
}
|
||||
|
||||
protected function buildProps(): array
|
||||
{
|
||||
$props = parent::buildProps();
|
||||
$name = $this->prop('name');
|
||||
if(!empty($name))
|
||||
{
|
||||
$props[] = setData('block', $name);
|
||||
$props[] = setClass("block-{$name}");
|
||||
$props[] = setID($this->prop('id'));
|
||||
}
|
||||
return $props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class btn extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'text?:string', // 按钮的文本。
|
||||
'icon?:string', // 图标名称。
|
||||
'iconClass?:string', // 图标的样式类。
|
||||
'square?:bool', // 是否为方形按钮,通常用于只显示一个图标的按钮。
|
||||
'disabled?:bool', // 是否禁用按钮。
|
||||
'active?:bool', // 是否为激活状态。
|
||||
'url?:string', // 按钮的链接地址。
|
||||
'target?:string', // 按钮的链接目标。
|
||||
'size?:string|int', // 按钮的尺寸,可选值为 `'xl'`、`'lg'`、`'md'`、`'sm'` 或者通过数字设置宽高,如 `20`。
|
||||
'trailingIcon?:string', // 按钮尾部图标的名称。
|
||||
'trailingIconClass?:string', // 按钮尾部图标的样式类。
|
||||
'caret?:string|bool', // 按钮的下拉箭头,可选值为 `'top'`(向上)、`'bottom'`(向下) 或者 `true`(自动)。
|
||||
'hint?:string', // 按钮的提示文本(鼠标悬停时显示)。
|
||||
'type?:string', // 按钮的类型,可选值为 `'default'`、`'primary'`、`'success'`、`'info'`、`'warning'`、`'danger'`、`'link'`。
|
||||
'btnType?:string="button"' // 按钮的类型,可选值为 `'button'`、`'submit'`、`'reset'`。
|
||||
);
|
||||
|
||||
public function onAddChild($child)
|
||||
{
|
||||
if(is_string($child) && !$this->props->has('text'))
|
||||
{
|
||||
$this->props->set('text', $child);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getProps()
|
||||
{
|
||||
$url = $this->prop('url');
|
||||
$target = $this->prop('target');
|
||||
$props = array_merge($this->getRestProps(), array('title' => $this->prop('hint')));
|
||||
|
||||
if(empty($url))
|
||||
{
|
||||
$props['type'] = $this->prop('btnType');
|
||||
if(!isset($props['data-target'])) $props['data-target'] = $target;
|
||||
return $props;
|
||||
}
|
||||
|
||||
$props['tagName'] = 'a';
|
||||
if(!isset($props['href'])) $props['href'] = $url;
|
||||
if(!isset($props['target'])) $props['target'] = $target;
|
||||
return $props;
|
||||
}
|
||||
|
||||
private function getChildren()
|
||||
{
|
||||
list($caret, $text, $icon, $iconClass, $trailingIcon, $trailingIconClass) = $this->prop(array('caret', 'text', 'icon', 'iconClass', 'trailingIcon', 'trailingIconClass'));
|
||||
|
||||
$children = array();
|
||||
if(!empty($icon)) $children[] = icon($icon, setClass($iconClass));
|
||||
if(!empty($text)) $children[] = h::span($text, setClass('text'));
|
||||
$children[] = parent::build();
|
||||
if(!empty($trailingIcon)) $children[] = icon($trailingIcon, setClass($trailingIconClass));
|
||||
if(!empty($caret)) $children[] = h::span(setClass(is_string($caret) ? "caret-$caret" : 'caret'));
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
protected function getClassList()
|
||||
{
|
||||
list($url, $type, $caret, $text, $icon, $trailingIcon) = $this->prop(array('url', 'type', 'caret', 'text', 'icon', 'trailingIcon'));
|
||||
$onlyCaret = empty($text) && !empty($caret) && empty($icon) && empty($trailingIcon);
|
||||
$classList = array(
|
||||
'btn' => true,
|
||||
'disabled' => $this->prop('disabled'),
|
||||
'active' => $this->prop('active'),
|
||||
'btn-caret' => $onlyCaret,
|
||||
'square' => $this->prop('square')
|
||||
);
|
||||
|
||||
if(empty($type) && !empty($url)) $type = 'btn-default';
|
||||
else if($type === 'link') $type = 'btn-link';
|
||||
else if($type === 'default') $type = 'btn-default';
|
||||
if(!empty($type)) $classList[$type] = true;
|
||||
|
||||
if(empty($text) && !empty($icon) && !isset($classList['square'])) $classList['square'] = true;
|
||||
|
||||
$size = $this->prop('size');
|
||||
if(!empty($size)) $classList["size-$size"] = true;
|
||||
|
||||
return $classList;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$props = $this->getProps();
|
||||
$children = $this->getChildren();
|
||||
$classList = $this->getClassList();
|
||||
|
||||
return button
|
||||
(
|
||||
set($props),
|
||||
setClass($classList),
|
||||
$children
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class btnGroup extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'items?:array',
|
||||
'disabled?:bool',
|
||||
'size?:string',
|
||||
);
|
||||
|
||||
public function onBuildItem($item): btn
|
||||
{
|
||||
if(!($item instanceof item)) $item = item(set($item));
|
||||
return btn(inherit($item));
|
||||
}
|
||||
|
||||
private function getClassName(): string
|
||||
{
|
||||
$disabled = $this->prop('disabled');
|
||||
$size = $this->prop('size');
|
||||
|
||||
$className = 'btn-group';
|
||||
if(!empty($disabled)) $className .= ' disabled';
|
||||
if(!empty($size)) $className .= " size-$size";
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$items = $this->prop('items');
|
||||
$className = $this->getclassName();
|
||||
|
||||
return div
|
||||
(
|
||||
setClass($className),
|
||||
set($this->getRestProps()),
|
||||
is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null,
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The burn widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author Yanyi Cao<caoyanyi@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
/**
|
||||
* 仪表盘(burn)部件类。
|
||||
* The burn widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class burn extends wg
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'data?: string|array', // 数据源
|
||||
'referenceLine?: bool=false' // 参考线
|
||||
);
|
||||
|
||||
/**
|
||||
* Build widget.
|
||||
*/
|
||||
protected function build(): zui
|
||||
{
|
||||
return zui::burn(inherit($this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class cell extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'flex?: string', // flex 类型或具体的值,例如:'auto'、'none'、'1'、'auto 1 1'。
|
||||
'order?: int', // flex-order 属性。
|
||||
'grow?: int', // flex-grow 属性。
|
||||
'shrink?: int', // flex-shrink 属性。
|
||||
'width?: string|int', // flex-basis 属性,支持数值或百分比,例如 128px、1/3、30%、128px。
|
||||
'align?: string' // align-self 属性,例如 'auto'、'flex-start'、'flex-end'、'center'、'baseline'、'stretch'。
|
||||
);
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$basis = null;
|
||||
$class = array('cell');
|
||||
$width = $this->prop('width');
|
||||
$flex = $this->prop('flex');
|
||||
if(!empty($width))
|
||||
{
|
||||
$basis = $width;
|
||||
if(is_numeric($width)) $basis = $width . 'px';
|
||||
elseif(preg_match('/^(\d+)\/(\d+)$/', $width, $matches) !== 0) $basis = ((int)$matches[1] / (int)$matches[2] * 100) . '%';
|
||||
}
|
||||
if(!empty($flex))
|
||||
{
|
||||
if(strpos($flex, ' ') !== false) $style['flex'] = $flex;
|
||||
else $class[] = "flex-$flex";
|
||||
}
|
||||
|
||||
$style = array();
|
||||
$style['order'] = $this->prop('order');
|
||||
$style['flex-grow'] = $this->prop('grow');
|
||||
$style['flex-shrink'] = $this->prop('shrink');
|
||||
$style['flex-basis'] = $basis;
|
||||
$style['align-self'] = $this->prop('align');
|
||||
|
||||
return div
|
||||
(
|
||||
setClass($class),
|
||||
setStyle($style),
|
||||
set($this->getRestProps()),
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class center extends wg
|
||||
{
|
||||
protected function build(): wg
|
||||
{
|
||||
return div
|
||||
(
|
||||
setClass("flex justify-center items-center"),
|
||||
set($this->getRestProps()),
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class checkbox extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'text?: string',
|
||||
'checked?: bool',
|
||||
'name?: string',
|
||||
'primary: bool=true',
|
||||
'id?: string',
|
||||
'disabled?: bool',
|
||||
'type: string="checkbox"',
|
||||
'value?: string',
|
||||
'typeClass?: string',
|
||||
'rootClass?: string',
|
||||
'labelClass?: string',
|
||||
);
|
||||
|
||||
public function onAddChild($child)
|
||||
{
|
||||
if(is_string($child) && !$this->props->has('text'))
|
||||
{
|
||||
$this->props->set('text', $child);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildPrimary()
|
||||
{
|
||||
list($id, $text, $name, $checked, $disabled, $type, $typeClass, $rootClass, $labelClass, $value) = $this->prop(array('id', 'text', 'name', 'checked', 'disabled', 'type', 'typeClass', 'rootClass', 'labelClass', 'value'));
|
||||
|
||||
if(empty($typeClass)) $typeClass = $type;
|
||||
if(empty($id)) $id = $name . '_' . $value;
|
||||
|
||||
return div
|
||||
(
|
||||
setClass("$typeClass-primary", $rootClass, array('disabled' => $disabled)),
|
||||
h::input
|
||||
(
|
||||
set::type($type),
|
||||
set::id($id),
|
||||
set::name($name),
|
||||
set::checked($checked),
|
||||
set($this->props->skip('text,primary,typeClass,rootClass,id,labelClass')),
|
||||
),
|
||||
h::label
|
||||
(
|
||||
set::for($id),
|
||||
setClass($labelClass),
|
||||
$text,
|
||||
),
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
if($this->prop('primary')) return $this->buildPrimary();
|
||||
list($text, $type, $typeClass) = $this->prop(array('text', 'type', 'typeClass'));
|
||||
|
||||
return h::label
|
||||
(
|
||||
setClass(empty($typeClass) ? $type : $typeClass),
|
||||
h::input
|
||||
(
|
||||
set::type($type),
|
||||
set($this->props->skip('text,primary,typeClass')),
|
||||
),
|
||||
is_string($text) ? span($text, set::class('text')) : $text,
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.checkbox-list {border-left: 1px solid var(--color-gray-400);}
|
||||
@@ -0,0 +1,41 @@
|
||||
window.handleCheckboxGroupClick = function(event)
|
||||
{
|
||||
const $target = $(event.target);
|
||||
const $checkboxGroup = $target.closest('.checkbox-group');
|
||||
if($target.closest('.checkbox-title').length > 0)
|
||||
{
|
||||
const $checkboxTitle = $target.closest('.checkbox-title');
|
||||
$checkboxGroup
|
||||
.find('.checkbox-child')
|
||||
.prop('checked', $checkboxTitle.prop('checked'));
|
||||
return;
|
||||
}
|
||||
|
||||
if($target.closest('.checkbox-child').length > 0)
|
||||
{
|
||||
let checkedCount = 0;
|
||||
const $checkboxChildren = $checkboxGroup.find('.checkbox-child');
|
||||
$checkboxChildren.each((_i, input) =>
|
||||
{
|
||||
if(input.checked === true) checkedCount++;
|
||||
});
|
||||
|
||||
const checkboxTitle = $checkboxGroup.find('.checkbox-title')[0];
|
||||
if(checkedCount === 0)
|
||||
{
|
||||
checkboxTitle.checked = false;
|
||||
checkboxTitle.indeterminate = false;
|
||||
}
|
||||
else if(checkedCount === $checkboxChildren.length)
|
||||
{
|
||||
$checkboxGroup.find('.checkbox-title').prop('checked', true);
|
||||
checkboxTitle.checked = true;
|
||||
checkboxTitle.indeterminate = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkboxTitle.checked = false;
|
||||
checkboxTitle.indeterminate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class checkboxGroup extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'title: array',
|
||||
'items: array'
|
||||
);
|
||||
|
||||
private static array $checkboxProps = array(
|
||||
'checked' => false,
|
||||
'disabled' => false,
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
|
||||
private function buildTitle(): wg
|
||||
{
|
||||
$title = array_merge(self::$checkboxProps, $this->prop('title'));
|
||||
return checkbox(set($title), setClass('checkbox-title'));
|
||||
}
|
||||
|
||||
private function buildCheckboxList(): wg
|
||||
{
|
||||
$items = $this->prop('items');
|
||||
$title = array_merge(self::$checkboxProps, $this->prop('title'));
|
||||
$list = ul(setClass('flex', 'flex-wrap', 'ml-1.5', 'checkbox-list', 'pl-3'));
|
||||
foreach($items as $item)
|
||||
{
|
||||
$item = array_merge(self::$checkboxProps, $item);
|
||||
if($title['checked'] === true) $item['checked'] = true;
|
||||
if($title['disabled'] === true) $item['disabled'] = true;
|
||||
$list->add
|
||||
(
|
||||
li
|
||||
(
|
||||
setClass('basis-1/2'),
|
||||
checkbox(set($item), setClass('checkbox-child'))
|
||||
)
|
||||
);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function build(): wg
|
||||
{
|
||||
return div
|
||||
(
|
||||
set('data-on', 'click'),
|
||||
set('data-call', 'window.handleCheckboxGroupClick'),
|
||||
set('data-params', 'event'),
|
||||
setClass('checkbox-group'),
|
||||
$this->buildTitle(),
|
||||
$this->buildCheckboxList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'checkbox' . DS . 'v1.php';
|
||||
|
||||
class checkList extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'primary: bool=true',
|
||||
'type: string="checkbox"',
|
||||
'name?: string',
|
||||
'value?: string|array',
|
||||
'items?: array',
|
||||
'inline?: bool'
|
||||
);
|
||||
|
||||
public function getValueList()
|
||||
{
|
||||
$value = $this->prop('value');
|
||||
if(is_null($value)) return array();
|
||||
|
||||
if($this->prop('type') === 'checkbox') return is_array($value) ? $value : explode(',', $value);
|
||||
return [$value];
|
||||
}
|
||||
|
||||
public function onBuildItem($item): checkbox
|
||||
{
|
||||
if($item instanceof item) $item = $item->props->toJsonData();
|
||||
|
||||
if(!isset($item['checked']))
|
||||
{
|
||||
$value = isset($item['value']) ? $item['value'] : '';
|
||||
$valueList = $this->getValueList();
|
||||
|
||||
$item['checked'] = in_array($value, $valueList);
|
||||
}
|
||||
|
||||
$props = $this->props->pick(['primary', 'type', 'name']);
|
||||
return new checkbox(set($props), set($item));
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
list($items, $inline) = $this->prop(['items', 'inline']);
|
||||
|
||||
if(!empty($items))
|
||||
{
|
||||
$valueList = $this->getValueList();
|
||||
foreach($items as $key => $item)
|
||||
{
|
||||
if(!is_array($item)) $item = array('text' => $item, 'value' => $key);
|
||||
if(!isset($item['checked'])) $item['checked'] = in_array($item['value'], $valueList);
|
||||
$items[$key] = $this->onBuildItem($item);
|
||||
}
|
||||
}
|
||||
|
||||
return div
|
||||
(
|
||||
setClass($inline ? 'check-list-inline' : 'check-list'),
|
||||
set($this->getRestProps()),
|
||||
$items,
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class col extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'justify?:string',
|
||||
'align?:string'
|
||||
);
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$classList = 'col';
|
||||
list($justify, $align) = $this->prop(array('justify', 'align'));
|
||||
if(!empty($justify)) $classList .= ' justify-' . $justify;
|
||||
if(!empty($align)) $classList .= ' items-' . $align;
|
||||
|
||||
return div
|
||||
(
|
||||
setClass($classList),
|
||||
set($this->getRestProps()),
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
|
||||
|
||||
class collapseBtn extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'target: string', // 展开折叠的目标元素选择器。
|
||||
'parent: string' // 目标元素与按钮共同的父级元素选择器,使用 closest 辅助目标元素的确定。
|
||||
);
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$target = $this->prop('target');
|
||||
$parent = $this->prop('parent');
|
||||
|
||||
return btn
|
||||
(
|
||||
setClass('btn-link', 'collapse-btn'),
|
||||
set($this->getRestProps()),
|
||||
set::icon('angle-down'),
|
||||
on::click
|
||||
(
|
||||
<<<FUNC
|
||||
const btn = event.target.closest('.collapse-btn');
|
||||
const icon = btn.querySelector('.icon');
|
||||
icon.classList.toggle('icon-angle-down');
|
||||
icon.classList.toggle('icon-angle-top');
|
||||
|
||||
const parentElm = btn.closest('$parent');
|
||||
const targetElm = parentElm.querySelector('$target');
|
||||
if(targetElm) targetElm.classList.toggle('hidden');
|
||||
FUNC
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The colorPicker widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
/**
|
||||
* 颜色选择器(colorPicker)部件类
|
||||
* The colorPicker widget class
|
||||
*/
|
||||
class colorPicker extends wg
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'id?: string="$GID"', // 组件根元素的 ID。
|
||||
'formID?: string', // 组件隐藏的表单元素 ID。
|
||||
'className?: string|array', // 类名。
|
||||
'style?: array', // 样式。
|
||||
'tagName?: string', // 组件根元素的标签名。
|
||||
'attrs?: array', // 附加到组件根元素上的属性。
|
||||
'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。
|
||||
'afterRender?: function', // 渲染完成后的回调函数。
|
||||
'beforeDestroy?: function', // 销毁前的回调函数。
|
||||
'name?: string', // 作为表单项的名称。
|
||||
'value?: string|string[]', // 默认值。
|
||||
'onChange?: function', // 值变更回调函数。
|
||||
'disabled?: boolean', // 是否禁用。
|
||||
'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。
|
||||
'required?: boolean', // 是否必选(不允许空值,不可以被清除)。
|
||||
'items?: string | string[]', // 颜色选项列表。
|
||||
'icon?: string|array="color"', // 将触发按钮显示为图标。
|
||||
'syncValue?: string', // 指定选择器同步颜色值作为文本到的元素。
|
||||
'syncColor?: string', // 指定选择器同步文字颜色到的元素。
|
||||
'syncBackground?: string', // 指定选择器同步背景颜色到的元素。
|
||||
'syncBorder?: string', // 指定选择器同步边框颜色到的元素。
|
||||
'hint?: string', // 提示文字。
|
||||
'closeBtn?: boolean', // 是否在弹出面板上显示关闭按钮。
|
||||
'heading?: ComponentChildren' // 弹出面板的标题。
|
||||
);
|
||||
|
||||
/**
|
||||
* Build widget.
|
||||
*
|
||||
* @access protected
|
||||
*/
|
||||
protected function build(): wg
|
||||
{
|
||||
list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList()));
|
||||
if(isset($props['id']))
|
||||
{
|
||||
$props['_id'] = $props['id'];
|
||||
unset($props['id']);
|
||||
}
|
||||
|
||||
if(!isset($props['items']))
|
||||
{
|
||||
global $app, $lang;
|
||||
$moduleName = $app->getModuleName();
|
||||
if(isset($lang->$moduleName->colorList)) $props['items'] = $lang->$moduleName->colorList;
|
||||
}
|
||||
return zui::colorPicker
|
||||
(
|
||||
set::_class('form-group-wrapper'),
|
||||
set::_map(array('value' => 'defaultValue', 'items' => 'colors', 'formID' => 'id')),
|
||||
set::_props($restProps),
|
||||
set($props),
|
||||
$this->children(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class commentBtn extends btn
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'dataTarget?:string',
|
||||
'dataUrl?:string',
|
||||
'dataType?:string',
|
||||
'icon?:string',
|
||||
'iconClass?:string',
|
||||
'text?:string',
|
||||
'square?:bool',
|
||||
'disabled?:bool',
|
||||
'active?:bool',
|
||||
'url?:string',
|
||||
'target?:string',
|
||||
'size?:string|int',
|
||||
'trailingIcon?:string',
|
||||
'trailingIconClass?:string',
|
||||
'caret?:string|bool',
|
||||
'hint?:string',
|
||||
'type?:string',
|
||||
'btnType?:string'
|
||||
);
|
||||
|
||||
protected function getProps(): array
|
||||
{
|
||||
$dataTarget = $this->prop('dataTarget');
|
||||
$dataUrl = $this->prop('dataUrl');
|
||||
$dataType = $this->prop('dataType');
|
||||
$props = parent::getProps();
|
||||
|
||||
$props['data-toggle'] = 'modal';
|
||||
$props['data-type'] = $dataType;
|
||||
$props['data-url'] = $dataUrl;
|
||||
$props['data-target'] = $dataTarget;
|
||||
|
||||
return $props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class commentDialog extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'title?:string',
|
||||
'url?:string',
|
||||
'name?:string="comment"',
|
||||
'method?:string="post"'
|
||||
);
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
global $lang;
|
||||
$title = $this->prop('title');
|
||||
$name = $this->prop('name');
|
||||
$url = $this->prop('url');
|
||||
$method = $this->prop('method');
|
||||
if(empty($title)) $title = $lang->action->create;
|
||||
|
||||
return modal
|
||||
(
|
||||
set::id('comment-dialog'),
|
||||
set::title($title),
|
||||
commentForm
|
||||
(
|
||||
set::url($url),
|
||||
set::method($method),
|
||||
set::name($name),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class commentForm extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'url?:string',
|
||||
'name?:string="comment"',
|
||||
'method?:string="POST"'
|
||||
);
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
global $lang;
|
||||
$url = $this->prop('url');
|
||||
$name = $this->prop('name');
|
||||
$method = $this->prop('method');
|
||||
if(empty($name)) $name = 'comment';
|
||||
|
||||
return form
|
||||
(
|
||||
set::url($url),
|
||||
set::method($method),
|
||||
setClass('comment-form'),
|
||||
editor
|
||||
(
|
||||
setID($name),
|
||||
set::name($name)
|
||||
),
|
||||
set::actions
|
||||
(
|
||||
array(
|
||||
'submit',
|
||||
array('data-dismiss' => 'modal', 'text' => $lang->close)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'textarea' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'editor' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'checkbox' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'checklist' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'radiolist' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'select' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'inputcontrol' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'picker' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'datepicker' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'timepicker' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'pripicker' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'severitypicker' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'colorpicker' . DS . 'v1.php';
|
||||
|
||||
class control extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'type?: string', // 表单输入元素类型,值可以为:static, text, password, email, number, date, time, datetime, month, url, search, tel, color, picker, pri, severity, select, checkbox, radio, checkboxList, radioList, checkboxListInline, radioListInline, file, textarea
|
||||
'name: string', // HTML name 属性
|
||||
'id?: string', // HTML id 属性
|
||||
'value?: string', // HTML value 属性
|
||||
'placeholder?: string', // HTML placeholder 属性
|
||||
'readonly?: bool', // HTML readonly 属性
|
||||
'required?: bool', // 是否为必填项
|
||||
'disabled?: bool', // 是否为禁用状态
|
||||
'items?: array' // 表单输入元素子项数据
|
||||
);
|
||||
|
||||
protected function created()
|
||||
{
|
||||
if($this->prop('id') === null && $this->prop('name') !== null)
|
||||
{
|
||||
$name = $this->prop('name');
|
||||
$id = substr($name, -2) == '[]' ? substr($name, 0, - 2) : $name;
|
||||
$this->setProp('id', $id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build control with static content.
|
||||
*
|
||||
* @return wg
|
||||
*/
|
||||
protected function buildStatic(): wg
|
||||
{
|
||||
return div
|
||||
(
|
||||
set::class('form-control-static'),
|
||||
set($this->props->skip(array('type', 'name', 'value', 'required', 'disabled', 'placeholder', 'items', 'required'))),
|
||||
set('data-name', $this->prop('name')),
|
||||
$this->prop('value')
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildTextarea(): wg
|
||||
{
|
||||
return new textarea(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function buildInputControl(): wg
|
||||
{
|
||||
$controlProps = array();
|
||||
$allProps = $this->props->skip('type');
|
||||
$propsNames = array_keys(inputControl::definedPropsList());
|
||||
|
||||
foreach($propsNames as $propName)
|
||||
{
|
||||
if(!isset($allProps[$propName])) continue;
|
||||
|
||||
$controlProps[$propName] = $allProps[$propName];
|
||||
unset($allProps[$propName]);
|
||||
}
|
||||
|
||||
return new inputControl
|
||||
(
|
||||
set($controlProps),
|
||||
new input(set($allProps)),
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildCheckbox(): wg
|
||||
{
|
||||
if($this->hasProp('items')) return $this->buildCheckList();
|
||||
return new checkList
|
||||
(
|
||||
new checkbox(set($this->props->skip('type')))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildCheckList(): wg
|
||||
{
|
||||
return new checkList
|
||||
(
|
||||
set($this->props->skip('type'))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildRadioList(): wg
|
||||
{
|
||||
return new radioList
|
||||
(
|
||||
set($this->props->skip('type'))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildCheckListInline(): wg
|
||||
{
|
||||
return new checkList
|
||||
(
|
||||
set::inline(true),
|
||||
set($this->props->skip('type'))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildRadioListInline(): wg
|
||||
{
|
||||
return new radioList
|
||||
(
|
||||
set::inline(true),
|
||||
set($this->props->skip('type'))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildDate(): wg
|
||||
{
|
||||
return new datePicker(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function buildTime(): wg
|
||||
{
|
||||
return new timePicker(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function buildPri(): wg
|
||||
{
|
||||
return new priPicker(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function buildSeverity(): wg
|
||||
{
|
||||
return new severityPicker(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function buildColor(): wg
|
||||
{
|
||||
return new colorPicker(set($this->props->skip('type')));
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$type = $this->prop('type');
|
||||
if(empty($type)) $type = $this->hasProp('items') ? 'picker' : 'text';
|
||||
|
||||
$methodName = "build{$type}";
|
||||
if(method_exists($this, $methodName)) return $this->$methodName();
|
||||
|
||||
$wgName = "\\zin\\$type";
|
||||
if(class_exists($wgName)) return new $wgName(set($this->props->skip('type')), $this->children());
|
||||
|
||||
return input(set($this->props));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The dashboard widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
/**
|
||||
* 仪表盘(dashboard)部件类。
|
||||
* The dashboard widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class dashboard extends wg
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'id?: string', // ID。
|
||||
'cache?: bool|string', // 是否启用缓存。
|
||||
'responsive?: bool', // 是否启用响应式。
|
||||
'blocks: array', // 区块列表。
|
||||
'grid?: int', // 栅格数。
|
||||
'gap?: int', // 间距。
|
||||
'leftStop?: int', // 区块水平停靠间隔。
|
||||
'cellHeight?: int', // 网格高度。
|
||||
'blockFetch?: string|function|array', // 区块数据获取 url 或选项。
|
||||
'blockDefaultSize?: array', // 区块默认大小。
|
||||
'blockSizeMap?: array', // 区块大小映射。
|
||||
'blockMenu?: array', // 区块菜单。
|
||||
'onLayoutChange?: function' // 布局变更事件。
|
||||
);
|
||||
|
||||
static $dashboardID = 0;
|
||||
|
||||
protected function created()
|
||||
{
|
||||
$this->setDefaultProps(array('id' => static::$dashboardID ? static::$dashboardID : 'dashboard', 'cache' => data('app.user.account')));
|
||||
static::$dashboardID++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build widget.
|
||||
*/
|
||||
protected function build(): wg
|
||||
{
|
||||
return zui::dashboard(set($this->props->skip(array('id'))), set('_id', $this->prop('id')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The datePicker widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 日期选择器(datePicker)部件类
|
||||
* The datePicker widget class
|
||||
*/
|
||||
class datePicker extends wg
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array
|
||||
(
|
||||
'id?: string="$GID"', // 组件根元素的 ID。
|
||||
'formID?: string', // 组件隐藏的表单元素 ID。
|
||||
'className?: string|array', // 类名。
|
||||
'style?: array', // 样式。
|
||||
'tagName?: string', // 组件根元素的标签名。
|
||||
'attrs?: array', // 附加到组件根元素上的属性。
|
||||
'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。
|
||||
'afterRender?: function', // 渲染完成后的回调函数。
|
||||
'beforeDestroy?: function', // 销毁前的回调函数。
|
||||
'name?: string', // 作为表单项的名称。
|
||||
'value?: string|string[]', // 默认值。
|
||||
'onChange?: function', // 值变更回调函数。
|
||||
'disabled?: boolean', // 是否禁用。
|
||||
'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。
|
||||
'required?: boolean', // 是否必选(不允许空值,不可以被清除)。
|
||||
'placeholder?: string', // 选择框上的占位文本。
|
||||
'format?: string', // 日期格式,默认 yyyy-MM-dd。
|
||||
'icon?: string|array="calendar"', // 在输入框右侧显示的图标。
|
||||
'weekNames?: string[]', // 星期名称,索引为 0 表示周日。
|
||||
'monthNames?: string[]', // 月份名称,索引为 0 表示一月份。
|
||||
'yearText?: string', // 用于显示年份的格式化文本。
|
||||
'todayText?: string', // 用于显示“今天”的文本。
|
||||
'clearText?: string', // 用于显示“清除”的文本。
|
||||
'weekStart?: int', // 一周从星期几开始,默认 1。
|
||||
'minDate?: string|int', // 最小可选的日期。
|
||||
'maxDate?: string|int', // 最大可选的日期。
|
||||
'menu?: array', // 左侧显示的菜单设置。
|
||||
'actions?: array', // 底部工具栏设置。
|
||||
'onInvalid?: function', // 日期值无效时的回调函数。
|
||||
);
|
||||
|
||||
/**
|
||||
* Build the widget.
|
||||
*
|
||||
* @access protected
|
||||
* @return wg
|
||||
*/
|
||||
protected function build(): wg
|
||||
{
|
||||
list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList()));
|
||||
if(isset($props['id']))
|
||||
{
|
||||
$props['_id'] = $props['id'];
|
||||
unset($props['id']);
|
||||
}
|
||||
|
||||
return zui::datePicker
|
||||
(
|
||||
set::_class('form-group-wrapper'),
|
||||
set::_map(array('value' => 'defaultValue', 'formID' => 'id')),
|
||||
set($props),
|
||||
set::_props($restProps),
|
||||
$this->children(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php';
|
||||
|
||||
class datetimePicker extends input
|
||||
{
|
||||
protected static array $defaultProps = array(
|
||||
'type' => 'datetime-local'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.detail-body > .form-actions:last-child {margin-top: 0;}
|
||||
@@ -0,0 +1,9 @@
|
||||
$(() =>
|
||||
{
|
||||
const $formActions = $('form.detail-body + .form-actions');
|
||||
if($formActions.length)
|
||||
{
|
||||
$detailBody = $formActions.prev();
|
||||
$detailBody.append($formActions);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class detailBody extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'isForm?: bool=false'
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array(
|
||||
'main' => array('map' => 'sectionList'),
|
||||
'side' => array('map' => 'detailSide'),
|
||||
'bottom' => array('map' => 'history,fileList'),
|
||||
'floating' => array('map' => 'floatToolbar'),
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$main = $this->block('main');
|
||||
$side = $this->block('side');
|
||||
$bottom = $this->block('bottom');
|
||||
$floating = $this->block('floating');
|
||||
$isForm = $this->prop('isForm');
|
||||
|
||||
if(!$isForm)
|
||||
{
|
||||
return div
|
||||
(
|
||||
setClass('detail-body rounded flex gap-1'),
|
||||
set($this->getRestProps()),
|
||||
div
|
||||
(
|
||||
setClass('col gap-1 grow'),
|
||||
$main,
|
||||
$bottom,
|
||||
center(setClass('pt-6'), $floating),
|
||||
),
|
||||
$side
|
||||
);
|
||||
}
|
||||
|
||||
return formBase
|
||||
(
|
||||
set::actionsClass('h-14 flex flex-none items-center justify-center shadow'),
|
||||
setClass('detail-body rounded col overflow-y-hidden bg-white'),
|
||||
set($this->getRestProps()),
|
||||
setStyle('height', 'calc(100vh - 120px)'),
|
||||
div
|
||||
(
|
||||
setClass('flex-auto overflow-y-auto'),
|
||||
div
|
||||
(
|
||||
setClass('flex'),
|
||||
setStyle('min-height', '100%'),
|
||||
div
|
||||
(
|
||||
setClass('col grow'),
|
||||
$main,
|
||||
$bottom,
|
||||
),
|
||||
div
|
||||
(
|
||||
setClass('w-1'),
|
||||
setStyle('background', 'var(--zt-page-bg)'),
|
||||
),
|
||||
$side,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class detailHeader extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'back?: string="APP"',
|
||||
'backUrl?: string',
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array(
|
||||
'prefix' => array(),
|
||||
'title' => array(),
|
||||
'suffix' => array(),
|
||||
);
|
||||
|
||||
private function backBtn(): wg
|
||||
{
|
||||
global $lang;
|
||||
return backBtn
|
||||
(
|
||||
set::icon('back'),
|
||||
set::type('secondary'),
|
||||
set::back($this->prop('back')),
|
||||
set::url($this->prop('backUrl')),
|
||||
$lang->goback
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$prefix = $this->block('prefix');
|
||||
$title = $this->block('title');
|
||||
$suffix = $this->block('suffix');
|
||||
|
||||
if(empty($prefix) && !isAjaxRequest('modal')) $prefix = $this->backBtn();
|
||||
|
||||
return div
|
||||
(
|
||||
setClass('detail-header flex justify-between mb-3'),
|
||||
div
|
||||
(
|
||||
setClass('flex', 'items-center', 'gap-x-4'),
|
||||
$prefix,
|
||||
$title,
|
||||
),
|
||||
$suffix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.detail-side {width: 370px;}
|
||||
.detail-side .tab-content>.tab-pane {padding-left: 0 !important;}
|
||||
.detail-side {background: #fff; height: min-content;}
|
||||
.detail-side .tabs:not(:first-child) {border-top: 1px solid #E6EAF1;}
|
||||
.detail-side .tabs {padding-top: 12px; padding-bottom: 20px;}
|
||||
.detail-side > .table-data {margin-top: 16px;}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class detailSide extends wg
|
||||
{
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
return div
|
||||
(
|
||||
setClass('detail-side flex-none px-6'),
|
||||
set($this->getRestProps()),
|
||||
$this->children()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.module-menu {max-height: calc(100vh - 105px);}
|
||||
.module-menu {padding: 0 0 8px;}
|
||||
.module-menu .active {color: var(--color-primary-600); font-weight: 500;}
|
||||
.module-menu header a:hover > .icon {color: var(--color-primary-600) !important;}
|
||||
.module-menu .tree-item * {white-space: nowrap;}
|
||||
|
||||
#docDropmenu .is-leading {display: none;}
|
||||
#docDropmenu .primary {--tw-ring-color: var(--btn-border-color); background-color: var(--btn-bg); color: inherit; width: 100%;}
|
||||
|
||||
.module-menu .tree .tree-item .tree-link {text-overflow: clip; overflow: hidden; flex: 1 10 auto;}
|
||||
.module-menu .tree .tree-item .tree-actions {margin-left: 0;}
|
||||
.module-menu .tree .tree-item .tree-actions .icon {display: none;}
|
||||
.module-menu .tree .tree-item .tree-item-content:hover .tree-actions .icon {display: block;}
|
||||
.module-menu .tree .tree-item .tree-item-content .tree-actions .with-popover-show .icon {display: block;}
|
||||
|
||||
.tree > .tree-item > .project-tree-title {font-size: 16px; margin-bottom: 0.5rem;}
|
||||
.tree > .tree-item > .project-tree-title .text {margin-left: 0.5rem;}
|
||||
.tree > .tree-item > .project-tree-title .tree-icon {color: var(--nav-active-color); opacity: 1;}
|
||||
.tree > .tree-item > .project-tree-title > .tree-toggle-icon {display: none;}
|
||||
@@ -0,0 +1,61 @@
|
||||
window.saveModule = function()
|
||||
{
|
||||
const name = $(this).val();
|
||||
if(!name) return $(this).closest('.tree-item').remove();
|
||||
|
||||
const {id, type, lib, module} = $(this).data();
|
||||
const parentID = $(this).data('parent');
|
||||
|
||||
const $element = $(`div[data-id='${id}']`);
|
||||
$.ajaxSubmit({
|
||||
url: $.createLink('tree', 'ajaxCreateModule'),
|
||||
data: {
|
||||
name : name,
|
||||
libID : lib,
|
||||
parentID : parentID,
|
||||
objectID : id,
|
||||
moduleType : module,
|
||||
isUpdate : false,
|
||||
createType : type,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addModule = function(id, addType)
|
||||
{
|
||||
const $element = $(`div[data-id='${id}']`);
|
||||
const {lib, type, module} = $element.data();
|
||||
|
||||
let parentID = ['docLib'].includes(type) ? '0' : $element.data('parent');
|
||||
if(addType == 'child') parentID = id;
|
||||
|
||||
const level = addType == 'same' ? $element.data('level') : $element.data('level') + 1;
|
||||
const style = `style="margin-left: calc(${level} * var(--tree-indent, 20px))"`;
|
||||
|
||||
let inputTpl = '<li class="tree-item">';
|
||||
inputTpl += `<div class="tree-item-content" ${style}>`;
|
||||
inputTpl += `<input id="moduleName" class="form-control" data-id="${id}" data-parent="${parentID}" data-type="${addType}" data-lib="${lib}" data-module="${module}">`;
|
||||
inputTpl += '</div></li>';
|
||||
|
||||
if(addType == 'same')
|
||||
{
|
||||
$(`div[data-id='${id}']`).before(inputTpl);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!$element.parent().hasClass('show')) $element.find('.tree-toggle-icon').trigger('click');
|
||||
|
||||
setTimeout(function()
|
||||
{
|
||||
if($element.next('.tree').length == 0) $element.after(`<menu class="tree" level="${level}" data-level="${level}"></menu>`);
|
||||
|
||||
$(`div[data-id='${id}']`).next('.tree').prepend(inputTpl);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
setTimeout(function()
|
||||
{
|
||||
$('#moduleName').trigger('focus');
|
||||
document.getElementById("moduleName").addEventListener('blur', saveModule);
|
||||
}, 1);
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class docMenu extends wg
|
||||
{
|
||||
private array $modules = array();
|
||||
|
||||
private array $mineTypes = array('mine', 'view', 'collect', 'createdby', 'editedby');
|
||||
|
||||
protected static array $defineProps = array(
|
||||
'modules: array',
|
||||
'activeKey?: int',
|
||||
'settingLink?: string',
|
||||
'closeLink: string',
|
||||
'menuLink: string',
|
||||
'title?: string',
|
||||
'linkParams?: string="%s"',
|
||||
'libID?: int=0',
|
||||
'moduleID?: int=0',
|
||||
'spaceType?: string',
|
||||
'objectType?: string',
|
||||
'objectID?: int=0',
|
||||
'hover?: bool=true',
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
|
||||
private function buildLink($item): string
|
||||
{
|
||||
$url = $item->url;
|
||||
if(!empty($url)) return $url;
|
||||
if(in_array($item->type, array('apiLib', 'docLib')))
|
||||
{
|
||||
$this->libID = $item->id;
|
||||
$this->moduleID = 0;
|
||||
}
|
||||
if($item->type == 'module') $this->moduleID = $item->id;
|
||||
|
||||
$linkParams = sprintf($this->linkParams, "libID={$this->libID}&moduleID={$this->moduleID}");
|
||||
if(in_array($this->spaceType, array('product', 'project', 'custom'))) $linkParams = "objectID={$this->objectID}&{$linkParams}";
|
||||
|
||||
$objectType = $this->objectType;
|
||||
|
||||
$moduleName = $this->spaceType == 'api' ? 'api' : 'doc';
|
||||
$methodName = '';
|
||||
if($this->spaceType == 'api')
|
||||
{
|
||||
$methodName = 'index';
|
||||
$linkParams = substr($linkParams, 1);
|
||||
}
|
||||
else if($item->type == 'annex')
|
||||
{
|
||||
$methodName = 'showFiles';
|
||||
$linkParams = "type={$objectType}&objectID={$item->objectID}";
|
||||
}
|
||||
else if(in_array($item->type, array('text', 'word', 'ppt', 'excel')))
|
||||
{
|
||||
$methodName = 'view';
|
||||
$linkParams = "docID={$this->moduleID}";
|
||||
}
|
||||
else if($objectType == 'execution')
|
||||
{
|
||||
$moduleName = 'execution';
|
||||
$methodName = 'doc';
|
||||
}
|
||||
else
|
||||
{
|
||||
$methodName = $this->spaceMethod[$objectType] ? $this->spaceMethod[$objectType] : 'teamSpace';
|
||||
if(in_array($objectType, $this->mineTypes))
|
||||
{
|
||||
$moduleID = $item->id;
|
||||
if(in_array($item->type, array('docLib', 'annex', 'api', 'execution'))) $moduleID = 0;
|
||||
|
||||
$type = in_array(strtolower($item->type), $this->mineTypes) ? strtolower($item->type) : 'mine';
|
||||
$linkParams = "type={$type}&libID={$this->libID}&moduleID={$moduleID}";
|
||||
}
|
||||
if($item->type == 'module' && $item->object == 'api')
|
||||
{
|
||||
$linkParams = str_replace(array('browseType=&', 'param=0'), array('browseType=byrelease&', "param={$this->release}"), $linkParams);
|
||||
}
|
||||
}
|
||||
return helper::createLink($moduleName, $methodName, $linkParams);
|
||||
}
|
||||
|
||||
private function buildMenuTree(array $items, int $parentID = 0): array
|
||||
{
|
||||
if(empty($items)) $items = $this->modules;
|
||||
if(empty($items)) return array();
|
||||
|
||||
$activeKey = $this->prop('activeKey');
|
||||
foreach($items as $setting)
|
||||
{
|
||||
$setting->parentID = $parentID;
|
||||
|
||||
$itemID = 0;
|
||||
if(!in_array(strtolower($setting->type), $this->mineTypes)) $itemID = $setting->id ? $setting->id : $parentID;
|
||||
|
||||
$item = array(
|
||||
'key' => $itemID,
|
||||
'text' => $setting->name,
|
||||
'icon' => $this->getIcon($setting),
|
||||
'url' => $this->buildLink($setting),
|
||||
'data-id' => $itemID,
|
||||
'data-lib' => $setting->type == 'docLib' ? $itemID : $setting->libID,
|
||||
'data-type' => $setting->type,
|
||||
'data-parent' => $setting->parentID,
|
||||
'data-module' => $this->currentModule,
|
||||
'active' => zget($setting, 'active', $itemID == $activeKey),
|
||||
'actions' => $this->getActions($setting)
|
||||
);
|
||||
|
||||
$children = zget($setting, 'children', array());
|
||||
if(!empty($children))
|
||||
{
|
||||
$children = $this->buildMenuTree($children, $itemID);
|
||||
$item['items'] = $children;
|
||||
}
|
||||
|
||||
$parentItems[] = $item;
|
||||
}
|
||||
return $parentItems;
|
||||
}
|
||||
|
||||
private function setMenuTreeProps(): void
|
||||
{
|
||||
global $app, $lang;
|
||||
$this->lang = $lang;
|
||||
$this->rawModule = $app->rawModule;
|
||||
$this->rawMethod = $app->rawMethod;
|
||||
$this->currentModule = $app->moduleName;
|
||||
|
||||
$this->release = $this->prop('release', 0);
|
||||
$this->libID = $this->prop('libID');
|
||||
$this->moduleID = $this->prop('moduleID');
|
||||
$this->modules = $this->prop('modules');
|
||||
$this->linkParams = $this->prop('linkParams', '%s');
|
||||
$this->spaceType = $this->prop('spaceType', '');
|
||||
$this->objectType = $this->prop('objectType', '');
|
||||
$this->objectID = $this->prop('objectID', 0);
|
||||
$this->spaceMethod = $this->prop('spaceMethod');
|
||||
|
||||
if($this->rawModule == 'api' && $this->rawMethod == 'view') $this->spaceType = 'api';
|
||||
if($this->spaceType != 'project')
|
||||
{
|
||||
$this->setProp('items', $this->buildMenuTree(array(), $this->libID));
|
||||
}
|
||||
else
|
||||
{
|
||||
$items = array();
|
||||
$index = 0;
|
||||
foreach($this->modules as $treeType => $modules)
|
||||
{
|
||||
if($treeType == 'project')
|
||||
{
|
||||
$treeTitle = $lang->projectCommon;
|
||||
$treeIcon = 'project';
|
||||
}
|
||||
elseif($treeType == 'execution')
|
||||
{
|
||||
$treeTitle = $lang->execution->common;
|
||||
$treeIcon = 'run';
|
||||
}
|
||||
else
|
||||
{
|
||||
$treeTitle = $lang->files;
|
||||
$treeIcon = 'paper-clip';
|
||||
}
|
||||
$items[] = array(
|
||||
'text' => $treeTitle,
|
||||
'icon' => $treeIcon,
|
||||
'class' => 'project-tree-title ' . ($index > 0 ? 'border-t mt-2 pt-2' : ''),
|
||||
);
|
||||
|
||||
$items = array_merge($items, $this->buildMenuTree($modules, $this->libID));
|
||||
$index ++;
|
||||
}
|
||||
$this->setProp('items', $items);
|
||||
}
|
||||
}
|
||||
|
||||
private function getActions($item): array|null
|
||||
{
|
||||
if(isset($item->hasAction) && !$item->hasAction) return null;
|
||||
if(in_array($item->type, array('mine', 'view', 'collect', 'createdBy', 'editedBy'))) return null;
|
||||
|
||||
$actions = $this->getOperateItems($item);
|
||||
if(empty($actions)) return null;
|
||||
|
||||
return array(
|
||||
array(
|
||||
'key' => 'more',
|
||||
'icon' => 'ellipsis-v',
|
||||
'type' => 'dropdown',
|
||||
'caret' => false,
|
||||
'dropdown' => array(
|
||||
'placement' => 'bottom-end',
|
||||
'items' => $actions,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getOperateItems($item): array
|
||||
{
|
||||
$menus = array();
|
||||
if(in_array($item->type, array('docLib', 'apiLib')))
|
||||
{
|
||||
$itemID = $item->id ? $item->id : $item->parentID;
|
||||
if(hasPriv($this->currentModule, 'addCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'adddirectory',
|
||||
'icon' => 'add-directory',
|
||||
'text' => $this->lang->doc->libDropdown['addModule'],
|
||||
'onClick' => jsRaw("() => addModule({$itemID}, 'child')")
|
||||
);
|
||||
}
|
||||
|
||||
if(hasPriv($this->currentModule, 'editCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'editlib',
|
||||
'icon' => 'edit',
|
||||
'text' => $this->lang->doc->libDropdown['editLib'],
|
||||
'data-toggle' => 'modal',
|
||||
'data-url' => createlink($this->currentModule, 'editlib', "libID={$itemID}"),
|
||||
);
|
||||
}
|
||||
|
||||
if(hasPriv($this->currentModule, 'deleteCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'dellib',
|
||||
'icon' => 'trash',
|
||||
'text' => $this->lang->doc->libDropdown['deleteLib'],
|
||||
'class' => 'ajax-submit',
|
||||
'data-url' => createLink($this->currentModule, 'deleteLib', "libID={$itemID}"),
|
||||
'data-confirm' => $this->lang->doc->confirmDeleteLib,
|
||||
);
|
||||
}
|
||||
}
|
||||
elseif($item->type == 'module')
|
||||
{
|
||||
if(hasPriv($this->currentModule, 'addCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'adddirectory',
|
||||
'icon' => 'add-directory',
|
||||
'text' => $this->lang->doc->libDropdown['addSameModule'],
|
||||
'onClick' => jsRaw("() => addModule({$item->id}, 'same')")
|
||||
);
|
||||
$menus[] = array(
|
||||
'key' => 'addsubdirectory',
|
||||
'icon' => 'add-directory',
|
||||
'text' => $this->lang->doc->libDropdown['addSubModule'],
|
||||
'onClick' => jsRaw("() => addModule({$item->id}, 'child')")
|
||||
);
|
||||
}
|
||||
|
||||
if(hasPriv($this->currentModule, 'editCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'editmodule',
|
||||
'icon' => 'edit',
|
||||
'text' => $this->lang->doc->libDropdown['editModule'],
|
||||
'link' => '',
|
||||
'data-toggle' => 'modal',
|
||||
'data-url' => createlink($this->currentModule, 'editCatalog', "moduleID={$item->id}&type=" . ($this->rawModule == 'api' ? 'api' : 'doc')),
|
||||
);
|
||||
}
|
||||
|
||||
if(hasPriv($this->currentModule, 'deleteCatalog'))
|
||||
{
|
||||
$menus[] = array(
|
||||
'key' => 'delmodule',
|
||||
'icon' => 'trash',
|
||||
'text' => $this->lang->doc->libDropdown['delModule'],
|
||||
'class' => 'ajax-submit',
|
||||
'data-url' => createLink($this->currentModule, 'deleteCatalog', "rootID={$item->parentID}&moduleID={$item->id}"),
|
||||
'data-confirm' => $this->lang->api->confirmDeleteLib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $menus;
|
||||
}
|
||||
|
||||
private function getIcon($item): string
|
||||
{
|
||||
$type = $item->type;
|
||||
if($type == 'apiLib') return 'interface-lib';
|
||||
if($type == 'docLib') return 'wiki-lib';
|
||||
if($type == 'annex') return 'annex-lib';
|
||||
if($type == 'execution') return 'execution';
|
||||
if($type == 'text') return 'file-text';
|
||||
if($type == 'word') return 'file-word';
|
||||
if($type == 'ppt') return 'file-powerpoint';
|
||||
if($type == 'excel') return 'file-excel';
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getTitle(): string
|
||||
{
|
||||
global $lang;
|
||||
$activeKey = $this->prop('activeKey');
|
||||
|
||||
if(empty($activeKey)) return $this->prop('title');
|
||||
|
||||
foreach($this->modules as $module)
|
||||
{
|
||||
if($module->id == $activeKey) return $module->name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function buildBtns(): wg|null
|
||||
{
|
||||
$settingLink = $this->prop('settingLink');
|
||||
$settingText = $this->prop('settingText');
|
||||
if(!$settingLink) return null;
|
||||
|
||||
global $app;
|
||||
$lang = $app->loadLang('datatable')->datatable;
|
||||
$currentModule = $app->rawModule;
|
||||
$currentMethod = $app->rawMethod;
|
||||
|
||||
if(!$settingText) $settingText = $lang->moduleSetting;
|
||||
|
||||
$datatableId = $app->moduleName . ucfirst($app->methodName);
|
||||
|
||||
return div
|
||||
(
|
||||
setClass('col gap-2 py-3 px-7'),
|
||||
$settingLink
|
||||
? a
|
||||
(
|
||||
setClass('btn'),
|
||||
setStyle('background', '#EEF5FF'),
|
||||
setStyle('box-shadow', 'none'),
|
||||
set('data-app', $app->tab),
|
||||
set('data-size', 'sm'),
|
||||
set('data-toggle', 'modal'),
|
||||
set::href($settingLink),
|
||||
$settingText
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
private function buildCloseBtn(): ?wg
|
||||
{
|
||||
$activeKey = $this->prop('activeKey');
|
||||
if(empty($activeKey)) return null;
|
||||
|
||||
return a
|
||||
(
|
||||
set('href', $this->prop('closeLink')),
|
||||
icon('close', setStyle('color', 'var(--color-slate-600)'))
|
||||
);
|
||||
}
|
||||
|
||||
private function buildDropDownMenu()
|
||||
{
|
||||
return menu
|
||||
(
|
||||
setID('dropdownMenu'),
|
||||
set::items(array())
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$this->setMenuTreeProps();
|
||||
$title = $this->getTitle();
|
||||
$menuLink = $this->prop('menuLink', '');
|
||||
|
||||
return div
|
||||
(
|
||||
setClass('module-menu rounded shadow-sm bg-white col rounded-sm'),
|
||||
$title && empty($menuLink) ? h::header
|
||||
(
|
||||
setClass('h-10 flex items-center pl-4 flex-none gap-3'),
|
||||
span
|
||||
(
|
||||
setClass('module-title text-lg font-semibold'),
|
||||
html($title)
|
||||
),
|
||||
$this->buildCloseBtn(),
|
||||
) : null,
|
||||
$menuLink ? dropmenu
|
||||
(
|
||||
set::id('docDropmenu'),
|
||||
set::text($title),
|
||||
set::url($menuLink),
|
||||
) : null,
|
||||
h::main
|
||||
(
|
||||
setClass($menuLink ? 'pt-3' : ''),
|
||||
setClass('col flex-auto overflow-y-auto overflow-x-hidden pl-4 pr-1'),
|
||||
zui::tree(set($this->props->pick(array('items', 'activeClass', 'activeIcon', 'activeKey', 'onClickItem', 'defaultNestedShow', 'changeActiveKey', 'isDropdownMenu', 'hover'))))
|
||||
),
|
||||
$this->buildBtns(),
|
||||
$this->buildDropDownMenu(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.hold {border: 1px dashed #ccc; box-sizing: border-box;}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class dragUl extends wg
|
||||
{
|
||||
private $ul;
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
protected function onAddChild($child)
|
||||
{
|
||||
if(!($child instanceof wg)) return false;
|
||||
|
||||
if($child->prop('tagName') !== 'li') return false;
|
||||
|
||||
$child->setProp('draggable', 'true');
|
||||
return $child;
|
||||
}
|
||||
|
||||
private function bindDragstartEvent()
|
||||
{
|
||||
$func = <<<DRAGSTART
|
||||
const dragLi = e.target;
|
||||
dragLi.style.opacity = .5;
|
||||
|
||||
const ul = document.querySelector('[data-zin-gid="{$this->ul->gid}"]');
|
||||
const liList = Array.from(ul.children);
|
||||
ul.dataset.dragIndex = liList.indexOf(dragLi);
|
||||
DRAGSTART;
|
||||
|
||||
$this->ul->add(on::dragstart($func));
|
||||
}
|
||||
|
||||
private function bindDragendEvent()
|
||||
{
|
||||
$func = <<<DRAGEND
|
||||
e.target.style.opacity = '';
|
||||
console.log('dragend');
|
||||
DRAGEND;
|
||||
$this->ul->add(on::dragend($func));
|
||||
}
|
||||
|
||||
private function bindDragoverEvent()
|
||||
{
|
||||
$func = <<<DRAGOVER
|
||||
e.preventDefault();
|
||||
DRAGOVER;
|
||||
$this->ul->add(on::dragover($func));
|
||||
}
|
||||
|
||||
private function bindDragexitEvent()
|
||||
{
|
||||
$func = <<<DRAGEXIT
|
||||
e.preventDefault();
|
||||
DRAGEXIT;
|
||||
$this->ul->add(on::dragexit($func));
|
||||
}
|
||||
|
||||
private function bindDragenterEvent()
|
||||
{
|
||||
$func = <<<DRAGENTER
|
||||
const enterLi = e.target.closest('li');
|
||||
enterLi.classList.add('hold');
|
||||
|
||||
const ul = document.querySelector('[data-zin-gid="{$this->ul->gid}"]');
|
||||
const liList = Array.from(ul.children);
|
||||
ul.dataset.enterIndex = liList.indexOf(enterLi);
|
||||
DRAGENTER;
|
||||
$this->ul->add(on::dragenter($func));
|
||||
}
|
||||
|
||||
private function bindDragleaveEvent()
|
||||
{
|
||||
$func = <<<DRAGLEAVE
|
||||
e.target.classList.remove('hold');
|
||||
DRAGLEAVE;
|
||||
$this->ul->add(on::dragleave($func));
|
||||
}
|
||||
|
||||
private function bindDropEvent()
|
||||
{
|
||||
$func = <<<DROP
|
||||
e.preventDefault();
|
||||
const ul = document.querySelector('[data-zin-gid="{$this->ul->gid}"]');
|
||||
const dragIndex = ul.dataset.dragIndex;
|
||||
const enterIndex = ul.dataset.enterIndex;
|
||||
const dragLi = Array.from(ul.children)[ul.dataset.dragIndex];
|
||||
const enterLi = Array.from(ul.children)[ul.dataset.enterIndex];
|
||||
enterLi.classList.remove('hold');
|
||||
if(dragIndex < enterIndex) {
|
||||
enterLi.after(dragLi);
|
||||
} else if(dragIndex > enterIndex) {
|
||||
enterLi.before(dragLi);
|
||||
}
|
||||
DROP;
|
||||
$this->ul->add(on::drop($func));
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$ul = ul
|
||||
(
|
||||
setClass('drag-ul'),
|
||||
set($this->getRestProps()),
|
||||
$this->children(),
|
||||
);
|
||||
|
||||
$ul->setProp('data-zin-gid', $ul->gid);
|
||||
$this->ul = $ul;
|
||||
$this->bindDragstartEvent();
|
||||
$this->bindDragendEvent();
|
||||
$this->bindDragoverEvent();
|
||||
$this->bindDragexitEvent();
|
||||
$this->bindDragenterEvent();
|
||||
$this->bindDragleaveEvent();
|
||||
$this->bindDropEvent();
|
||||
return $ul;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'menu' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
|
||||
|
||||
class dropdown extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'items?:array',
|
||||
'placement?:string',
|
||||
'strategy?:string',
|
||||
'offset?: int',
|
||||
'flip?: bool',
|
||||
'arrow?: string',
|
||||
'trigger?: string',
|
||||
'menu?: array',
|
||||
'target?: string',
|
||||
'id?: string',
|
||||
'menuClass?: string',
|
||||
'hasIcons?: bool',
|
||||
'staticMenu?: bool'
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array
|
||||
(
|
||||
'trigger' => array('map' => 'btn,a'),
|
||||
'menu' => array('map' => 'menu'),
|
||||
'items' => array('map' => 'item')
|
||||
);
|
||||
|
||||
protected function build(): array
|
||||
{
|
||||
list($items, $placement, $strategy, $offset, $flip, $arrow, $trigger, $menuProps, $target, $id, $menuClass, $hasIcons, $staticMenu) = $this->prop(array('items', 'placement', 'strategy', 'offset', 'flip', 'arrow', 'trigger', 'menu', 'target', 'id', 'menuClass', 'hasIcons', 'staticMenu'));
|
||||
|
||||
$triggerBlock = $this->block('trigger');
|
||||
$menu = $this->block('menu');
|
||||
$itemsList = $this->block('items');
|
||||
|
||||
if(empty($id)) $id = $this->gid;
|
||||
if(empty($target) && empty($items)) $target = "#$id";
|
||||
if(empty($menuProps)) $menuProps = array();
|
||||
|
||||
if(empty($triggerBlock)) $triggerBlock = h::a($this->children());
|
||||
elseif(is_array($triggerBlock)) $triggerBlock = $triggerBlock[0];
|
||||
$triggerID = '';
|
||||
if($triggerBlock instanceof wg)
|
||||
{
|
||||
if($triggerBlock instanceof btn) $triggerBlock->setDefaultProps(array('caret' => true));
|
||||
$triggerBlock->setProp($this->getRestProps());
|
||||
|
||||
$triggerProps = array
|
||||
(
|
||||
'data-target' => $triggerBlock->hasProp('target', 'href') ? null : $target,
|
||||
'data-toggle' => 'dropdown',
|
||||
'data-placement' => $placement,
|
||||
'data-strategy' => $strategy,
|
||||
'data-offset' => $offset,
|
||||
'data-flip' => $flip,
|
||||
'data-arrow' => $arrow,
|
||||
'data-trigger' => $trigger
|
||||
);
|
||||
$triggerBlock->setProp($triggerProps);
|
||||
|
||||
$triggerID = $triggerBlock->id();
|
||||
if(empty($triggerID))
|
||||
{
|
||||
$triggerID = "$id-toggle";
|
||||
$triggerBlock->setProp('id', $triggerID);
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($menu))
|
||||
{
|
||||
if($staticMenu)
|
||||
{
|
||||
$menu = new menu
|
||||
(
|
||||
setClass('dropdown-menu'),
|
||||
set::items($items),
|
||||
divorce($itemsList),
|
||||
);
|
||||
|
||||
if($hasIcons === null)
|
||||
{
|
||||
if(is_array($items))
|
||||
{
|
||||
foreach($items as $item)
|
||||
{
|
||||
if((is_array($item) and isset($item['icon'])) || (($item instanceof wg) && $item->hasProp('icon')))
|
||||
{
|
||||
$hasIcons = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!$hasIcons)
|
||||
{
|
||||
foreach($itemsList as $item)
|
||||
{
|
||||
if(($item instanceof wg) && $item->hasProp('icon'))
|
||||
{
|
||||
$hasIcons = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(empty($items)) $items = array();
|
||||
if(!empty($itemsList))
|
||||
{
|
||||
foreach($itemsList as $item)
|
||||
{
|
||||
if(!($item instanceof item)) continue;
|
||||
$items[] = $item->props->toJsonData();
|
||||
}
|
||||
}
|
||||
foreach($items as $index => $item)
|
||||
{
|
||||
if(!isset($item['icon']) || empty($item['icon']) || str_starts_with($item['icon'], 'icon-')) continue;
|
||||
$items[$index]['icon'] = 'icon-' . $item['icon'];
|
||||
}
|
||||
|
||||
if(!is_array($menuProps)) $menuProps = array();
|
||||
$menuProps['items'] = $items;
|
||||
|
||||
$menu = zui::dropdown
|
||||
(
|
||||
set(array
|
||||
(
|
||||
'_to' => "#$triggerID",
|
||||
'trigger' => $trigger,
|
||||
'placement' => $placement,
|
||||
'strategy' => $strategy,
|
||||
'arrow' => $arrow,
|
||||
'flip' => $flip,
|
||||
'offset' => $offset,
|
||||
'target' => $target,
|
||||
'className' => $menuClass,
|
||||
'hasIcons' => $hasIcons,
|
||||
'menu' => $menuProps
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
elseif(is_array($menu))
|
||||
{
|
||||
$menu = $menu[0];
|
||||
}
|
||||
|
||||
if($menu instanceof menu)
|
||||
{
|
||||
$menu->setProp($menuProps);
|
||||
$menu->setProp('class', $menuClass);
|
||||
$menu->setProp('id', $id);
|
||||
if($hasIcons) $menu->setProp('class', 'has-icons');
|
||||
}
|
||||
|
||||
return array($triggerBlock, $menu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pick-pop-admin-menu {width: 130px !important;}
|
||||
#pick-pop-admin-menu .dropmenu-list {padding: 0;}
|
||||
#pick-pop-admin-menu .admin-menu-item > .dropmenu-item {padding-left: 10px !important;}
|
||||
#pick-pop-admin-menu .admin-menu-item > .dropmenu-item.active {background-color: unset;}
|
||||
#pick-pop-admin-menu .admin-menu-item > .dropmenu-item:hover {color: rgba(var(--color-primary-500-rgb),var(--tw-text-opacity)); background-color: rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The dropmenu widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
/**
|
||||
* 1.5 级导航(dropmenu)部件类。
|
||||
* The dropmenu widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class dropmenu extends wg
|
||||
{
|
||||
/**
|
||||
* Define the properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array
|
||||
(
|
||||
'id?: string="dropmenu"', // ID,当页面有多个 dropmenu 时确保有唯一的 ID。
|
||||
'tab?: string,', // 应用名。
|
||||
'module?: string,', // 模块名。
|
||||
'method?: string,', // 方法名。
|
||||
'objectID?: string,', // 对象 ID。
|
||||
'extra?: string,', // 额外参数。
|
||||
'url?: string', // 异步获取下拉菜单选项数据的 URL,如果已经指定 module, method,objectID,extra 等参数则可以忽略。
|
||||
'text?: string', // 选择按钮上显示的文本。
|
||||
'cache?: bool|int=true', // 是否启用缓存。
|
||||
'data?: array', // 手动指定数据。
|
||||
'menuID?: string="$GID"', // 指定下拉菜单的ID。
|
||||
);
|
||||
|
||||
/**
|
||||
* Load the css file.
|
||||
*
|
||||
* @access public
|
||||
* @return string|false
|
||||
*/
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the build method.
|
||||
*
|
||||
* @access protected
|
||||
* @return wg
|
||||
*/
|
||||
protected function build(): zui
|
||||
{
|
||||
list($url, $text, $objectID, $cache, $tab, $module, $method, $extra, $id, $data, $menuID) = $this->prop(array('url', 'text', 'objectID', 'cache', 'tab', 'module', 'method', 'extra', 'id', 'data', 'menuID'));
|
||||
|
||||
$app = data('app');
|
||||
$lang = data('lang');
|
||||
|
||||
if(empty($tab)) $tab = $app->tab;
|
||||
if(empty($module)) $module = $app->rawModule;
|
||||
if(empty($method)) $method = $app->rawMethod;
|
||||
if(empty($extra)) $extra = '';
|
||||
if(empty($objectID)) $objectID = data($tab . 'ID');
|
||||
if(empty($objectID))
|
||||
{
|
||||
$object = data($tab);
|
||||
if(isset($object->id)) $objectID = $object->id;
|
||||
}
|
||||
|
||||
if($tab == 'admin')
|
||||
{
|
||||
$currentMenuKey = $app->control->loadModel('admin')->getMenuKey();
|
||||
$text = $lang->admin->menuList->{$currentMenuKey}['name'];
|
||||
$url = createLink('admin', 'ajaxGetDropMenu', "currentMenuKey={$currentMenuKey}");
|
||||
$menuID = 'admin-menu';
|
||||
}
|
||||
|
||||
if(empty($url) && empty($data)) $url = createLink($tab, 'ajaxGetDropMenu', "objectID=$objectID&module=$module&method=$method&extra=$extra");
|
||||
if(empty($text) && !empty($tab) && !empty($objectID))
|
||||
{
|
||||
$object = $app->control->loadModel($tab)->getByID((int)$objectID);
|
||||
$text = $object->name;
|
||||
}
|
||||
|
||||
return zui::dropmenu
|
||||
(
|
||||
setID($menuID),
|
||||
set('_id', $id),
|
||||
set('_props', array('data-fetcher' => $url)),
|
||||
set('data', $data),
|
||||
set(array('fetcher' => $url, 'text' => $text, 'defaultValue' => $objectID, 'cache' => $cache)),
|
||||
set($this->getRestProps())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.menu.menu-dtable-actions {display: flex; min-width: auto;}
|
||||
.menu.menu-dtable-actions .icon {position: static!important; opacity: 1!important;}
|
||||
.menu.menu-dtable-actions span.text {display: none;}
|
||||
.menu.menu-dtable-actions > .menu-item > a {padding: 0;}
|
||||
.menu.menu-dtable-actions > .menu-item > a:hover {background: none; color: var(--menu-hover-bg);}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class dtable extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'className?:string="shadow rounded"', // 表格样式。
|
||||
'id?:string', // ID。
|
||||
'customCols?: bool|array', // 是否支持自定义列。
|
||||
'cols?:array', // 表格列配置
|
||||
'data?:array', // 表格数据源
|
||||
'module?:string', // 模块信息,主要是获取语言项
|
||||
'emptyTip?:string', // 表格数据源为空时显示的文本
|
||||
'createLink?:array|string', // 表格数据源为空时的创建链接
|
||||
'createTip?:string', // 表格数据源为空时显示的文本
|
||||
);
|
||||
|
||||
static $dtableID = 0;
|
||||
|
||||
protected function created()
|
||||
{
|
||||
global $app;
|
||||
|
||||
$defaultID = "table-$app->rawModule-$app->rawMethod";
|
||||
$this->setDefaultProps(array('id' => static::$dtableID ? ($defaultID . static::$dtableID) : $defaultID));
|
||||
static::$dtableID++;
|
||||
|
||||
if($this->prop('customCols') === true)
|
||||
{
|
||||
$app->loadLang('datatable');
|
||||
$this->setProp('customCols', array(
|
||||
'custom' => array(
|
||||
'url' => createLink('datatable', 'ajaxcustom', "module=$app->moduleName&method=$app->methodName"),
|
||||
'text' => $app->lang->datatable->custom
|
||||
),
|
||||
'setGlobal' => array(
|
||||
'url' => createLink('datatable', 'ajaxsaveglobal', "module={$app->moduleName}&method={$app->methodName}"),
|
||||
'text' => $app->lang->datatable->setGlobal,
|
||||
),
|
||||
'reset' => array(
|
||||
'url' => createLink('datatable', 'ajaxreset', "module={$app->moduleName}&method={$app->methodName}"),
|
||||
'text' => $app->lang->datatable->reset,
|
||||
),
|
||||
'resetGlobal' => array(
|
||||
'url' => createLink('datatable', 'ajaxreset', "module={$app->moduleName}&method={$app->methodName}&system=1"),
|
||||
'text' => $app->lang->datatable->resetGlobal,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
$module = $this->prop('module', $app->rawModule);
|
||||
if(!isset($app->lang->$module)) $app->loadLang($module);
|
||||
|
||||
/* Set col default name and title. */
|
||||
$colConfigs = $this->prop('cols');
|
||||
foreach($colConfigs as $field => &$config)
|
||||
{
|
||||
if(is_object($config)) $config = (array)$config;
|
||||
|
||||
if(!isset($config['name'])) $config['name'] = $field;
|
||||
if(!isset($config['title'])) $config['title'] = zget($app->lang->{$module}, $config['name'], zget($app->lang, $config['name']));
|
||||
if(isset($config['link']) && is_array($config['link'])) $config['link'] = $this->getLink($config['link']);
|
||||
if(isset($config['assignLink']) && is_array($config['assignLink'])) $config['assignLink'] = $this->getLink($config['assignLink']);
|
||||
|
||||
if(!empty($config['type']) && $config['type'] == 'control')
|
||||
{
|
||||
if(!empty($config['control']) && is_string($config['control'])) $config['control'] = array('type' => $config['control']);
|
||||
|
||||
if(isset($config['controlItems']))
|
||||
{
|
||||
if(empty($config['control'])) $config['control'] = array('type' => 'picker');
|
||||
|
||||
$items = $config['controlItems'];
|
||||
$newItems = array();
|
||||
foreach($items as $key => $value)
|
||||
{
|
||||
if(is_numeric($key) && is_array($value)) $newItems[] = $value;
|
||||
else $newItems[] = array('text' => $value, 'value' => $key);
|
||||
}
|
||||
$config['control']['props']['items'] = $newItems;
|
||||
unset($config['controlItems']);
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($config['actionsMap']))
|
||||
{
|
||||
foreach($config['actionsMap'] as &$action)
|
||||
{
|
||||
if(isset($action['data-toggle']) && !isset($action['data-position'])) $action['data-position'] = 'center';
|
||||
|
||||
if(!empty($action['ajaxSubmit']))
|
||||
{
|
||||
if(empty($action['className'])) $action['className'] = 'ajax-submit';
|
||||
if(!isset($action['data-confirm'])) $action['data-confirm'] = zget($app->lang->$module, 'confirmDelete');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->setProp('cols', array_values($colConfigs));
|
||||
|
||||
$tableData = $this->prop('data', array());
|
||||
$this->setProp('data', array_values($tableData));
|
||||
|
||||
/* Add dtable load info to pager links. */
|
||||
$pager = $this->prop('footPager');
|
||||
if(!empty($pager) && isset($pager['items']))
|
||||
{
|
||||
if(!isset($pager['btnProps'])) $pager['btnProps'] = array('data-load' => 'table', 'type' => 'ghost', 'size' => 'sm');
|
||||
foreach($pager['items'] as $index => $item)
|
||||
{
|
||||
if($item['type'] !== 'size-menu') continue;
|
||||
if(isset($item['itemProps'])) $pager['items'][$index]['itemProps']['data-load'] = 'table';
|
||||
else $pager['items'][$index]['itemProps'] = array('data-load' => 'table');
|
||||
}
|
||||
$this->setProp('footPager', $pager);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段链接。
|
||||
* Get link to the field.
|
||||
*
|
||||
* @param array $setting
|
||||
* @access protected
|
||||
* @return array|string
|
||||
*/
|
||||
protected function getLink(array $setting): array|string
|
||||
{
|
||||
if(!empty($setting['url']))
|
||||
{
|
||||
$url = $setting['url'];
|
||||
if(!empty($url['module']) && !empty($url['method']))
|
||||
{
|
||||
$setting['url'] = '';
|
||||
if(hasPriv($url['module'], $url['method'])) $setting['url'] = createLink($url['module'], $url['method'], zget($url, 'params', ''), '', !empty($setting['onlybody']));
|
||||
}
|
||||
return $setting;
|
||||
}
|
||||
else if(!empty($setting['module']) && !empty($setting['method']))
|
||||
{
|
||||
if(!hasPriv($setting['module'], $setting['method'])) return '';
|
||||
return createLink($setting['module'], $setting['method'], zget($setting, 'params', ''), '', !empty($setting['onlybody']));
|
||||
}
|
||||
return $setting;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
if(empty($this->prop('data')))
|
||||
{
|
||||
global $lang;
|
||||
$createLink = !empty($this->prop('createLink')) ? $this->getLink($this->prop('createLink')) : '';
|
||||
return div
|
||||
(
|
||||
setClass('canvas text-center py-8'),
|
||||
p
|
||||
(
|
||||
setClass('py-8 my-8'),
|
||||
span
|
||||
(
|
||||
setClass('text-gray'),
|
||||
!empty($this->prop('emptyTip')) ? $this->prop('emptyTip') : $lang->noData,
|
||||
),
|
||||
!empty($createLink)
|
||||
? a
|
||||
(
|
||||
setClass('btn primary-pale bd-primary ml-0.5'),
|
||||
set::href($createLink),
|
||||
icon('plus'),
|
||||
!empty($this->prop('createTip')) ? $this->prop('createTip') : $lang->create,
|
||||
)
|
||||
: '',
|
||||
)
|
||||
);
|
||||
}
|
||||
return zui::dtable(inherit($this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.dynamic > li {position: relative; list-style: none}
|
||||
.dynamic > li:before, .dynamic > li > div:after {position: absolute; left: -20px; display: block; width: 15px; height: 15px; content: ' '; border-radius: 50%}
|
||||
.dynamic > li:before {top: 12px; left: -17px; z-index: 3; width: 10px; height: 10px; background-color: #c4c4c4; border: none; border: 2px solid #fff;}
|
||||
.dynamic > li > div:after {top: 17px; left: -15px; z-index: 3; width: 6px; height: 6px; background-color: var(--color-gray-300); border-radius: 50%; opacity: 0}
|
||||
.dynamic > li.blue > div:after {background-color: #2e7fff;}
|
||||
.dynamic > li.green > div:after {background-color: rgb(25, 190, 131);}
|
||||
.dynamic > li.yellow > div:after {background-color: rgb(255, 210, 169);}
|
||||
.dynamic > li.trophy > div:after {background-color: unset;}
|
||||
.dynamic > li:after {position: absolute; top: 12px; bottom: -13px; left: -13px; z-index: 1; display: block; content: ' '; border-left: 2px solid #eee;}
|
||||
.dynamic > li > div:after {opacity: 1}
|
||||
.dynamic > li:before {top: 12px; left: -20px; width: 16px; height: 16px; background-color: var(--color-slate-100); border: none;}
|
||||
.dynamic > li.trophy:before {background: url('static/svg/trophy.svg') no-repeat; width: 18px; height: 18px; background-size: 100%; left: -21px; top: 13px; background-color: var(--color-slate-100);}
|
||||
.dynamic > li > div {display: block; padding: 3px 0px; line-height: 20px}
|
||||
.dynamic > li .dynamic-text {padding: 2px 5px;}
|
||||
.dynamic > li.trophy .dynamic-text {background: #fff;}
|
||||
.dynamic > li.trophy .dynamic-text img {opacity: 0.2; transform: rotate(6deg); position: relative; max-width: 2.25rem; top: -15px; left: -35px; width: 2.25rem; height: 2.25rem;}
|
||||
|
||||
.dynamic-tag {position: absolute; top: 5px; left: -110px; font-size: 12px}
|
||||
.dynamic-tag-left {padding-left: 110px;}
|
||||
.dynamic-tag.has-time {left: -65px;}
|
||||
.has-time .dynamic-tag-left {padding-left: 65px;}
|
||||
|
||||
.dynamic-sm {font-size: 12px}
|
||||
.dynamic-sm > li:before, .dynamic-sm > li > div:after {top: 10px; left: -20px; width: 11px; height: 11px}
|
||||
.dynamic-sm > li.blue:before, .dynamic-sm > li:before {top: 10px; left: -18px; width: 11px; height: 11px; background: 0 0; border: 1px solid #eee}
|
||||
.dynamic-sm > li > div {line-height: 20px}
|
||||
.dynamic-sm > li > div:after {top: 13px; left: -15px; width: 5px; height: 5px}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class dynamic extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'dynamics?: array',
|
||||
'users?: array',
|
||||
'className?: string',
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public function getStatusClass(object $dynamic): string
|
||||
{
|
||||
$action = strtolower($dynamic->action);
|
||||
$objectType = strtolower($dynamic->objectType);
|
||||
|
||||
// if($dynamic->major) return 'active';
|
||||
|
||||
if($objectType == 'release' && $action == 'opened') return 'trophy';
|
||||
if($objectType == 'project' && $action == 'closed') return 'trophy';
|
||||
|
||||
if(strpos($action, 'assigned') !== false) return 'blue';
|
||||
if(strpos($action, 'finished') !== false || strpos($action, 'resolved') !== false || ($action == 'closed' && $objectType != 'product')) return 'green';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function dynamicItem(object $dynamic, array $users): wg
|
||||
{
|
||||
global $config;
|
||||
$dynamicLabel = zget($dynamic, 'dynamicLabel', '');
|
||||
if(empty($dynamicLabel)) $dynamicLabel = zget($dynamic, 'actionLabel', '');
|
||||
|
||||
$objectLabel = array();
|
||||
if($dynamic->action != 'login' && $dynamic->action != 'logout')
|
||||
{
|
||||
$objectLabel[] = span
|
||||
(
|
||||
$dynamic->objectLabel,
|
||||
);
|
||||
$objectID = $dynamic->objectID && strpos(',module,chartgroup,', ",$dynamic->objectType,") !== false && strpos(',created,edited,moved,', "$dynamic->action") !== false ? trim($dynamic->extra, ',') : $dynamic->objectID;
|
||||
$objectLabel[] = $objectID ? span
|
||||
(
|
||||
setClass('label light-outline mx-2 font-sm'),
|
||||
$objectID,
|
||||
) : null;
|
||||
|
||||
if(($config->edition == 'max' && strpos($config->action->assetType, ",{$dynamic->objectType},") !== false) && empty($dynamic->objectName))
|
||||
{
|
||||
$objectLabel[] = span("#{$dynamic->objectID}");
|
||||
}
|
||||
elseif(empty($dynamic->objectID) and $dynamic->extra)
|
||||
{
|
||||
$objectLabel[] = span("#{$dynamic->extra}");
|
||||
}
|
||||
elseif(empty($dynamic->objectLink))
|
||||
{
|
||||
$objectLabel[] = span($dynamic->objectName);
|
||||
}
|
||||
else
|
||||
{
|
||||
$objectLabel[] = a
|
||||
(
|
||||
set::href($dynamic->objectLink),
|
||||
set::title($dynamic->objectName),
|
||||
$dynamic->objectName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$dynamicClass = $this->getStatusClass($dynamic);
|
||||
return li
|
||||
(
|
||||
setClass($dynamicClass),
|
||||
div
|
||||
(
|
||||
span
|
||||
(
|
||||
setClass('dynamic-tag p-1 text-gray'),
|
||||
isset($dynamic->time) ? $dynamic->time : $dynamic->date,
|
||||
),
|
||||
div
|
||||
(
|
||||
setClass('dynamic-text flex flex-nowrap justify-between items-center'),
|
||||
div
|
||||
(
|
||||
setClass('clip p-1'),
|
||||
zget($users, $dynamic->actor),
|
||||
span
|
||||
(
|
||||
setClass('text-gray px-1'),
|
||||
$dynamicLabel
|
||||
),
|
||||
$objectLabel,
|
||||
),
|
||||
$dynamicClass == 'trophy' ?
|
||||
div
|
||||
(
|
||||
setClass('w-0 h-0'),
|
||||
h::img
|
||||
(
|
||||
set::src('static/svg/trophy.svg'),
|
||||
setClass('ml-2'),
|
||||
)
|
||||
) : null
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$users = $this->prop('users', (array)data('users'));
|
||||
$dynamics = $this->prop('dynamics', (array)data('dynamics'));
|
||||
$hasTime = !empty($dynamisc) && isset(reset($dynamics)->time) ? 'has-time' : '';
|
||||
|
||||
$dynamicListView = h::ul
|
||||
(
|
||||
setClass('dynamic dynamic-tag-left pt-0 overflow-hidden has-time'),
|
||||
setClass($this->prop('className')),
|
||||
);
|
||||
|
||||
foreach($dynamics as $dynamic)
|
||||
{
|
||||
if($dynamic->action == 'adjusttasktowait') continue;
|
||||
$dynamicListView->add($this->dynamicItem($dynamic, $users));
|
||||
}
|
||||
|
||||
return $dynamicListView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class echarts extends wg
|
||||
{
|
||||
public function size(string|int $width, string|int $height): echarts
|
||||
{
|
||||
if(is_numeric($width)) $width = "{$width}px";
|
||||
if(is_numeric($height)) $height = "{$height}px";
|
||||
$this->setProp('_size', array($width, $height));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function theme(string|array $value): echarts
|
||||
{
|
||||
$this->setProp('theme', $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function responsive(bool $value): echarts
|
||||
{
|
||||
$this->setProp('responsive', $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function build(): zui
|
||||
{
|
||||
global $app;
|
||||
$jsFile = $app->getWebRoot() . 'js/echarts/echarts.common.min.js';
|
||||
|
||||
return zui::echarts(inherit($this), set::_call("~((name,selector,options) => $.getScript('$jsFile', null, () => zui.create(name,selector,options)))"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
textarea[size="sm"],
|
||||
tiptap-editor[size="sm"] {min-height: 142px;}
|
||||
textarea[size="lg"],
|
||||
tiptap-editor[size="lg"] {min-height: 250px;}
|
||||
|
||||
[data-tippy-root] tiptap-menu-item > button {font-size: inherit; font-family: Arial; line-height: normal;}
|
||||
|
||||
.editor-container {height: auto; width: 100%;}
|
||||
.editor-container tiptap-editor {display: none;}
|
||||
.editor-container textarea {height: initial;}
|
||||
[data-loaded-editor] .editor-container tiptap-editor {display: block;}
|
||||
[data-loaded-editor] .editor-container textarea {display: none;}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'textarea' . DS . 'v1.php';
|
||||
|
||||
class editor extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'createInput?: bool=true', // 是否创建一个隐藏的 input 存储编辑器内容
|
||||
'uploadUrl?: string=""', // 图片上传链接
|
||||
'placeholder?: string=""', // 占位文本
|
||||
'fullscreenable?: bool=true', // 是否可全屏
|
||||
'resizable?: bool=true', // 是否可自适应
|
||||
'exposeEditor?: bool=true', // 是否将编辑器实例挂载到 window
|
||||
'size?: string="sm"', // 尺寸
|
||||
'hideMenubar?: bool=false', // 是否隐藏 menubar
|
||||
'bubbleMenu?: bool=false', // 是否启用菜单冒泡
|
||||
'menubarMode?: string="compact"', // 菜单栏模式
|
||||
'value?: string' // 内容
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
// global $app;
|
||||
// $jsFile = $app->getWebRoot() . 'js/zeneditor/tiptap-component.esm.js';
|
||||
$jsFile = 'https://zui-dist.oop.cc/zeneditor/tiptap-component.esm.js';
|
||||
return '$.getScript("' . $jsFile . '", {type: "module"}, () => {document.body.dataset.loadedEditor = true;});';
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$editor = new h
|
||||
(
|
||||
setTag('tiptap-editor'),
|
||||
setClass('form-control', 'p-0', 'h-auto'),
|
||||
);
|
||||
$props = $this->props->pick(array('createInput', 'uploadUrl', 'placeholder', 'fullscreenable', 'resizable', 'exposeEditor', 'size', 'hideMenubar', 'bubbleMenu', 'menubarMode', 'collaborative', 'hocuspocus', 'docName', 'username', 'userColor'));
|
||||
foreach($props as $key => $value)
|
||||
{
|
||||
if($value === true || (is_string($value) && !empty($value))) $editor->add(set(uncamelize($key), $value));
|
||||
}
|
||||
|
||||
$customProps = $this->getRestProps();
|
||||
if(!isset($customProps['id'])) $customProps['id'] = $customProps['name'];
|
||||
if(!isset($customProps['class'])) $customProps['class'] = 'w-full';
|
||||
|
||||
$editor->add(set($customProps));
|
||||
$editor->add($this->prop('value'));
|
||||
$editor->add($this->children());
|
||||
|
||||
return div
|
||||
(
|
||||
setClass('editor-container'),
|
||||
$editor,
|
||||
textarea
|
||||
(
|
||||
$this->prop('value'),
|
||||
set::rows(1),
|
||||
set::size($props['size'])
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.entity-label .label {box-shadow: none; border: 1px solid var(--color-slate-300); color: var(--color-slate-700); background: none;}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'label' . DS . 'v1.php';
|
||||
|
||||
class entityLabel extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'entityID?: string|int', // 实体编号
|
||||
'level?: string|int', // 标题层级
|
||||
'text: string', // 实体文本
|
||||
'reverse?: bool=false', // 编号与文本是否交换顺序
|
||||
'textClass?: string', // 文本样式类
|
||||
'idClass?: string', // 编号样式类
|
||||
'href?: string', // 实体链接
|
||||
'titlePrefix?: array', // 标题前缀
|
||||
'labelProps?: array' // 标签属性
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array(
|
||||
'prefix' => array(),
|
||||
'suffix' => array()
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
protected function onAddChild(mixed $child): mixed
|
||||
{
|
||||
if(is_string($child) && !$this->props->has('text'))
|
||||
{
|
||||
$this->props->set('text', $child);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->props->addToList('titlePrefix', $child);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildEntityID(): ?wg
|
||||
{
|
||||
$entityID = $this->prop('entityID');
|
||||
$className = $this->prop('idClass');
|
||||
if(!isset($entityID)) return null;
|
||||
|
||||
return new label
|
||||
(
|
||||
setClass('justify-center rounded-full px-1.5 h-3.5', $className),
|
||||
$entityID
|
||||
);
|
||||
}
|
||||
|
||||
private function buildEntityName(): wg
|
||||
{
|
||||
$text = $this->prop('text');
|
||||
$level = $this->prop('level');
|
||||
$className = $this->prop('textClass');
|
||||
$href = $this->prop('href');
|
||||
$labelProps = $this->prop('labelProps');
|
||||
$titlePrefix = $this->prop('titlePrefix');
|
||||
|
||||
$titleClass = empty($level)
|
||||
? "article-content"
|
||||
: "article-h$level";
|
||||
|
||||
if(empty($href)) return div
|
||||
(
|
||||
setClass($titleClass, $className),
|
||||
set($labelProps),
|
||||
$titlePrefix,
|
||||
$text
|
||||
);
|
||||
|
||||
return a
|
||||
(
|
||||
setClass($titleClass, $className),
|
||||
set::href($href),
|
||||
set($labelProps),
|
||||
$titlePrefix,
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$reverse = $this->prop('reverse');
|
||||
$prefix = $this->block('prefix');
|
||||
$suffix = $this->block('suffix');
|
||||
$entityID = $this->buildEntityID();
|
||||
$entityName = $this->buildEntityName();
|
||||
return div
|
||||
(
|
||||
setClass('entity-label', 'flex', 'items-center', 'gap-x-2'),
|
||||
set($this->getRestProps()),
|
||||
$prefix,
|
||||
$reverse ? array($entityName, $entityID) : array($entityID, $entityName),
|
||||
$suffix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'nav' . DS . 'v1.php';
|
||||
|
||||
class featureBar extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'items?:array',
|
||||
'current?:string',
|
||||
'link?:string',
|
||||
'current?:string',
|
||||
'linkParams?:string=""',
|
||||
'module?:string',
|
||||
'method?:string',
|
||||
'load?: string="table"',
|
||||
'loadID?: string'
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array
|
||||
(
|
||||
'nav' => array('map' => 'nav'),
|
||||
'leading' => array(),
|
||||
'trailing' => array()
|
||||
);
|
||||
|
||||
protected function getItems()
|
||||
{
|
||||
$items = $this->prop('items');
|
||||
if(!empty($items)) return $items;
|
||||
|
||||
global $app, $lang;
|
||||
$currentModule = $this->prop('module', $app->rawModule);
|
||||
$currentMethod = $this->prop('method', $app->rawMethod);
|
||||
|
||||
\common::sortFeatureMenu($currentModule, $currentMethod);
|
||||
|
||||
$rawItems = \customModel::getFeatureMenu($currentModule, $currentMethod);
|
||||
if(!is_array($rawItems)) return null;
|
||||
|
||||
$current = $this->prop('current', data('browseType'));
|
||||
$pager = data('pager');
|
||||
$recTotal = $pager ? $pager->recTotal : data('recTotal');
|
||||
$items = array();
|
||||
$link = $this->prop('link');
|
||||
$loadID = $this->prop('loadID');
|
||||
$load = $this->prop('load');
|
||||
|
||||
data('activeFeature', $current);
|
||||
|
||||
if(empty($link)) $link = createLink($app->rawModule, $app->rawMethod, $this->prop('linkParams'));
|
||||
|
||||
foreach($rawItems as $item)
|
||||
{
|
||||
if(isset($item->hidden)) continue;
|
||||
|
||||
$isActive = $item->name == $current;
|
||||
|
||||
$moreSelects = array();
|
||||
if($item->name == 'more' && !empty($lang->$currentModule->moreSelects)) $moreSelects = $lang->$currentModule->moreSelects;
|
||||
if(isset($lang->$currentModule->moreSelects[$currentMethod][$item->name])) $moreSelects = $lang->$currentModule->moreSelects[$currentMethod][$item->name];
|
||||
if($item->name == 'QUERY' && !empty($lang->custom->queryList)) $moreSelects = $lang->custom->queryList;
|
||||
if(!empty($moreSelects))
|
||||
{
|
||||
$activeText = $item->text;
|
||||
|
||||
$subItems = array();
|
||||
$callback = $this->prop($item->name == 'more' ? 'moreMenuLinkCallback' : 'queryMenuLinkCallback');
|
||||
$callback = isset($callback[0]) ? $callback[0] : null;
|
||||
|
||||
foreach($moreSelects as $key => $text)
|
||||
{
|
||||
$subItem = array();
|
||||
$subItem['text'] = $text;
|
||||
$subItem['active'] = $key == $current;
|
||||
$subItem['url'] = ($callback instanceof \Closure) ? $callback($key, $text) : str_replace('{key}', (string)$key, $link);
|
||||
$subItem['attrs'] = ['data-id' => $key, 'data-load' => $load, 'data-target' => $loadID];
|
||||
|
||||
if($item->name == 'QUERY')
|
||||
{
|
||||
$closeLink = createLink('search', 'ajaxRemoveMenu', "queryID={$key}");
|
||||
$loadUrl = $subItem['url'] . '#featureBar';
|
||||
|
||||
$subItem['className'] = 'flex-auto';
|
||||
$subItem['rootClass'] = 'row gap-0';
|
||||
$subItem['rootChildren'] = array(jsRaw("zui.h('a', {className: 'ajax-submit', 'data-url': '{$closeLink}', 'data-load': '{$loadUrl}'}, zui.h('span', {className: 'close'}))"));
|
||||
}
|
||||
|
||||
$subItems[] = $subItem;
|
||||
|
||||
if($key === $current)
|
||||
{
|
||||
$isActive = true;
|
||||
$activeText = $text;
|
||||
}
|
||||
}
|
||||
|
||||
$items[] = array
|
||||
(
|
||||
'text' => $activeText,
|
||||
'active' => $isActive,
|
||||
'type' => 'dropdown',
|
||||
'caret' => 'down',
|
||||
'items' => $subItems,
|
||||
'badge' => $isActive && $recTotal != '' ? array('text' => $recTotal, 'class' => 'size-sm rounded-full white') : null,
|
||||
'props' => array('data-id' => $item->name)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$items[] = array
|
||||
(
|
||||
'text' => $item->text,
|
||||
'active' => $isActive,
|
||||
'url' => str_replace('{key}', $item->name, $link),
|
||||
'badge' => $isActive && $recTotal != '' ? array('text' => $recTotal, 'class' => 'size-sm rounded-full white') : null,
|
||||
'props' => array('data-id' => $item->name, 'data-load' => $load, 'data-target' => $loadID)
|
||||
);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function buildNav()
|
||||
{
|
||||
$nav = $this->block('nav');
|
||||
if(!empty($nav) && $nav[0] instanceof nav) return $nav;
|
||||
return new nav
|
||||
(
|
||||
set::class('nav-feature'),
|
||||
set::items($this->getItems()),
|
||||
divorce($this->children())
|
||||
);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
return div
|
||||
(
|
||||
set::id('featureBar'),
|
||||
$this->block('leading'),
|
||||
$this->buildNav(),
|
||||
$this->block('trailing')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php';
|
||||
|
||||
class fileInput extends input
|
||||
{
|
||||
protected static array $defaultProps = array(
|
||||
'name' => 'file',
|
||||
'type' => 'file'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.file {padding-top: 2px;}
|
||||
ul.files-list {margin-bottom: unset}
|
||||
.files-list>li>a {display: inline; word-wrap: break-word; color: #313c52; line-height: 24px}
|
||||
.files-list>li>.right-icon {opacity: 1;}
|
||||
.fileAction {color: #0c64eb !important;}
|
||||
.renameFile {display: flex;}
|
||||
.renameFile .input-group {margin-left: 10px;}
|
||||
.renameFile .input-group-addon {width: 60px;}
|
||||
.renameFile > .icon { margin-top: 10px;}
|
||||
.backgroundColor {background: #eff5ff; }
|
||||
.icon.icon-file-text {padding-left: 7px}
|
||||
.right-icon .btn {padding: 0 6px; height: 20px}
|
||||
@@ -0,0 +1,127 @@
|
||||
$(document).ready(function()
|
||||
{
|
||||
$('li.file').on('mouseover', function()
|
||||
{
|
||||
$(this).children('span.right-icon').removeClass("hidden");
|
||||
$(this).addClass('backgroundColor');
|
||||
});
|
||||
|
||||
$('li.file').on('mouseout', function()
|
||||
{
|
||||
$(this).children('span.right-icon').addClass("hidden");
|
||||
$(this).removeClass('backgroundColor');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*
|
||||
* @param int $fileID
|
||||
* @param object $obj
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
window.deleteFile = function(fileID, obj)
|
||||
{
|
||||
if(!fileID) return;
|
||||
|
||||
const method = $(obj).closest('.files-list').parent().data('method');
|
||||
const showDelete = $(obj).closest('.files-list').parent().data('showDelete');
|
||||
|
||||
if(showDelete && method == 'edit')
|
||||
{
|
||||
$('<input />').attr('type', 'hidden').attr('name', 'deleteFiles[' + fileID + ']').attr('value', fileID).appendTo('ul.files-list');
|
||||
$(obj).closest('li.file').addClass('hidden');
|
||||
}
|
||||
else
|
||||
{
|
||||
$.ajaxSubmit(
|
||||
{
|
||||
url:$.createLink('file', 'delete', 'fileID=' + fileID),
|
||||
load:true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file, append the mouse to the link. Thus we call decide to open the file in browser no download it.
|
||||
*
|
||||
* @param int $fileID
|
||||
* @param int $extension
|
||||
* @param int $imageWidth
|
||||
* @param string $fileTitle
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
window.downloadFile = function(fileID, extension, imageWidth, fileTitle)
|
||||
{
|
||||
if(!fileID) return;
|
||||
const sessionString = $('ul.files-list').parent().data('session');
|
||||
|
||||
var fileTypes = 'txt,jpg,jpeg,gif,png,bmp';
|
||||
var windowWidth = $(window).width();
|
||||
var width = (windowWidth > imageWidth) ? ((imageWidth < windowWidth * 0.5) ? windowWidth * 0.5 : imageWidth) : windowWidth;
|
||||
var checkExtension = fileTitle.lastIndexOf('.' + extension) == (fileTitle.length - extension.length - 1);
|
||||
|
||||
var url = $.createLink('file', 'download', 'fileID=' + fileID + '&mouse=left');
|
||||
url += url.indexOf('?') >= 0 ? '&' : '?';
|
||||
url += sessionString;
|
||||
|
||||
if(fileTypes.indexOf(extension) >= 0 && checkExtension)
|
||||
{
|
||||
loadModal(url);
|
||||
}
|
||||
else
|
||||
{
|
||||
loadPage(url, '_blank');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edit box for editing file name.
|
||||
*
|
||||
* @param int $fileID
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
window.showRenameBox = function(fileID)
|
||||
{
|
||||
$('#renameFile' + fileID).closest('li').addClass('hidden');
|
||||
$('#renameBox' + fileID).closest('li').removeClass('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show File.
|
||||
*
|
||||
* @param int $fileID
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
window.showFile = function(fileID)
|
||||
{
|
||||
$('#renameBox' + fileID).closest('li').addClass('hidden');
|
||||
$('#renameFile' + fileID).closest('li').removeClass('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth refresh file name.
|
||||
*
|
||||
* @param int $fileID
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
window.setFileName = function(fileID)
|
||||
{
|
||||
var fileName = $('#fileName' + fileID).val();
|
||||
var extension = $('#extension' + fileID).val();
|
||||
var postData = {'fileName' : fileName, 'extension' : extension};
|
||||
$.ajaxSubmit(
|
||||
{
|
||||
url:$.createLink('file', 'edit', 'fileID=' + fileID),
|
||||
dataType: 'json',
|
||||
method: 'post',
|
||||
data: postData,
|
||||
load:true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'section' . DS . 'v1.php';
|
||||
|
||||
class fileList extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'files?:array',
|
||||
'fieldset?:bool=true',
|
||||
'method?:string="view"',
|
||||
'showDelete?:bool=true',
|
||||
'showEdit?:bool=true',
|
||||
'object?:object',
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
|
||||
private function fileList(): wg
|
||||
{
|
||||
global $app;
|
||||
|
||||
$files = $this->prop('files');
|
||||
$method = $this->prop('method');
|
||||
$showDelete = $this->prop('showDelete');
|
||||
$showEdit = $this->prop('showEdit');
|
||||
$object = (object)$this->prop('object');
|
||||
$fileListView = h::ul(setClass('files-list col relative'));
|
||||
|
||||
foreach($files as $file)
|
||||
{
|
||||
$fileItemView = li
|
||||
(
|
||||
setClass('mb-2'),
|
||||
html($app->loadTarget('file')->printFile($file, $method, $showDelete, $showEdit, $object))
|
||||
);
|
||||
|
||||
$fileListView->add($fileItemView);
|
||||
}
|
||||
|
||||
return $fileListView;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
global $lang;
|
||||
|
||||
$fieldset = $this->prop('fieldset');
|
||||
$isInModal = isAjaxRequest('modal');
|
||||
$px = $isInModal ? 'px-3' : 'px-6';
|
||||
$pb = $isInModal ? 'pb-3' : 'pb-6';
|
||||
|
||||
$method = $this->prop('method');
|
||||
$showDelete = $this->prop('showDelete');
|
||||
|
||||
$fileDiv = div
|
||||
(
|
||||
set
|
||||
(
|
||||
array(
|
||||
'data-method' => $method,
|
||||
'data-showDelete' => $showDelete,
|
||||
'data-session' => session_name() . '=' . session_id(),
|
||||
)
|
||||
),
|
||||
$this->fileList()
|
||||
);
|
||||
|
||||
return $fieldset ? new section
|
||||
(
|
||||
setClass('files', 'pt-4', $px, $pb, 'canvas'),
|
||||
set::title($lang->files),
|
||||
to::actions
|
||||
(
|
||||
icon('paper-clip'),
|
||||
),
|
||||
$fileDiv
|
||||
) : $fileDiv;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.float-btn {background: rgba(49, 60, 82, 0.7); font-size: 24px; height: 20px; width: 20px; border-radius: 50%; opacity: 0.8;}
|
||||
.float-btn .icon {font-size: 18px; color: white;}
|
||||
@@ -0,0 +1,24 @@
|
||||
function updateBtnPosition(margin)
|
||||
{
|
||||
const preBtn = document.querySelector('#preButton');
|
||||
const nextBtn = document.querySelector('#nextButton');
|
||||
|
||||
if(preBtn)
|
||||
{
|
||||
$(preBtn).css('top', margin);
|
||||
}
|
||||
|
||||
if(nextBtn)
|
||||
{
|
||||
$(nextBtn).css('top', margin);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function(event)
|
||||
{
|
||||
updateBtnPosition(event.target.innerHeight / 2);
|
||||
});
|
||||
|
||||
$(() => {
|
||||
updateBtnPosition(window.innerHeight / 2);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class floatPreNextBtn extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'preLink?:string',
|
||||
'nextLink?:string',
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
public static function getPageJS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
|
||||
}
|
||||
protected function build(): wg
|
||||
{
|
||||
$preLink = $this->prop('preLink');
|
||||
$nextLink = $this->prop('nextLink');
|
||||
|
||||
return fragment
|
||||
(
|
||||
!empty($preLink) ? btn
|
||||
(
|
||||
setID('preButton'),
|
||||
set::url($preLink),
|
||||
setClass('float-btn fixed left-0 z-10'),
|
||||
set::icon('angle-left')
|
||||
) : null,
|
||||
!empty($nextLink) ? btn
|
||||
(
|
||||
setID('nextButton'),
|
||||
set::url($nextLink),
|
||||
setClass('float-btn fixed right-0 z-10'),
|
||||
set::icon('angle-right')
|
||||
) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.float-toolbar {background: rgba(49, 60, 82, 0.7);}
|
||||
.float-toolbar>.divider {background: #838A9D;}
|
||||
.float-toolbar>.btn:before {background: rgba(0, 0, 0, .1);}
|
||||
.float-toolbar>.btn:hover {color: #FFF;}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
class floatToolbar extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'prefix?:array',
|
||||
'main?:array',
|
||||
'suffix?:array',
|
||||
'object?:object'
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array(
|
||||
'prefix' => array(),
|
||||
'main' => array(),
|
||||
'suffix' => array(),
|
||||
);
|
||||
|
||||
public static function getPageCSS(): string|false
|
||||
{
|
||||
return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
|
||||
}
|
||||
|
||||
private function buildDivider(wg|array|null $wg): wg|null
|
||||
{
|
||||
if(empty($wg)) return null;
|
||||
|
||||
return div(setClass('divider w-px h-6 mx-2'));
|
||||
}
|
||||
|
||||
private function buildBtns(array|null $items): array|null
|
||||
{
|
||||
if(empty($items)) return null;
|
||||
|
||||
$btns = array();
|
||||
foreach ($items as &$item)
|
||||
{
|
||||
if(!$item) continue;
|
||||
|
||||
if(!empty($item['url'])) $item['url'] = preg_replace_callback('/\{(\w+)\}/', array($this, 'getObjectValue'), $item['url']);
|
||||
if(!empty($item['data-url'])) $item['data-url'] = preg_replace_callback('/\{(\w+)\}/', array($this, 'getObjectValue'), $item['data-url']);
|
||||
|
||||
$btns[] = btn(set($item), setClass('ghost text-white'));
|
||||
}
|
||||
return $btns;
|
||||
}
|
||||
|
||||
public function getObjectValue($matches)
|
||||
{
|
||||
if(!isset($this->object)) $this->object = $this->prop('object');
|
||||
|
||||
return zget($this->object, $matches[1]);
|
||||
}
|
||||
|
||||
private function mergeBtns(array|null $btns, array|wg|null $block): array|wg|null
|
||||
{
|
||||
if(empty($btns) && empty($block)) return null;
|
||||
if(empty($block)) return $btns;
|
||||
|
||||
if($block[0] instanceof btn)
|
||||
{
|
||||
$block[0]->add(setClass('ghost', 'text-white'));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach($block[0]->children() as $blockBtn) $blockBtn->add(setClass('ghost', 'text-white'));
|
||||
}
|
||||
if(empty($btns)) return $block;
|
||||
|
||||
if(!is_array($block)) $block = array($block);
|
||||
return array_merge($btns, $block);
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
$prefixBtns = $this->buildBtns($this->prop('prefix'));
|
||||
$mainBtns = $this->buildBtns($this->prop('main'));
|
||||
$suffixBtns = $this->buildBtns($this->prop('suffix'));
|
||||
|
||||
$prefixBlock = $this->block('prefix');
|
||||
$mainBlock = $this->block('main');
|
||||
$suffixBlock = $this->block('suffix');
|
||||
|
||||
$prefixBtns = $this->mergeBtns($prefixBtns, $prefixBlock);
|
||||
$mainBtns = $this->mergeBtns($mainBtns, $mainBlock);
|
||||
$suffixBtns = $this->mergeBtns($suffixBtns, $suffixBlock);
|
||||
|
||||
return div
|
||||
(
|
||||
setClass('float-toolbar inline-flex rounded p-1.5 items-center'),
|
||||
$prefixBtns,
|
||||
$this->buildDivider($prefixBtns),
|
||||
$mainBtns,
|
||||
empty($mainBtns) ? null : $this->buildDivider($suffixBtns),
|
||||
$suffixBtns,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'formgroup' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'formrow' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'formbase' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 通用表单(form)部件类,支持 Ajax 提交
|
||||
* The common form widget class
|
||||
*/
|
||||
class form extends formBase
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'items?: array', // 使用一个列定义对象数组来定义表单项。
|
||||
'grid?: bool=true', // 是否启用网格部件,禅道中所有表单都是网格布局,除非有特殊目的,无需设置此项。
|
||||
'labelWidth?: int', // 标签宽度,单位为像素。
|
||||
'actionsClass?: string="form-group no-label"' // 操作按钮栏的 CSS 类。
|
||||
);
|
||||
|
||||
protected function created()
|
||||
{
|
||||
parent::created();
|
||||
|
||||
if(!isAjaxRequest('modal')) return;
|
||||
|
||||
global $app, $lang;
|
||||
$module = $app->getModuleName();
|
||||
$method = $app->getMethodName();
|
||||
$text = !empty($lang->$module->$method) ? $lang->$module->$method : zget($lang, $method, '');
|
||||
|
||||
$defaultProps = array();
|
||||
$defaultProps['submitBtnText'] = $text;
|
||||
$defaultProps['class'] = 'px-3 pb-4';
|
||||
|
||||
$this->setDefaultProps($defaultProps);
|
||||
}
|
||||
|
||||
public function onBuildItem(item|array $item): wg
|
||||
{
|
||||
if(!($item instanceof item))
|
||||
{
|
||||
if($item instanceof wg) return $item;
|
||||
$item = item(set($item));
|
||||
}
|
||||
|
||||
if($this->prop('grid')) return new formRow(inherit($item));
|
||||
|
||||
return new formGroup(inherit($item));
|
||||
}
|
||||
|
||||
protected function buildActions(): wg|null
|
||||
{
|
||||
$actions = parent::buildActions();
|
||||
if($this->prop('grid') && !empty($actions)) $actions = div(setClass('form-row'), $actions);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
protected function buildProps(): array
|
||||
{
|
||||
list($grid, $labelWidth) = $this->prop(array('grid', 'labelWidth'));
|
||||
$props = parent::buildProps();
|
||||
if($grid) $props[] = setClass('form-grid');
|
||||
if(!empty($labelWidth)) $props[] = setCssVar('form-grid-label-width', $labelWidth);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
protected function buildContent(): array
|
||||
{
|
||||
list($items, $grid) = $this->prop(array('items', 'grid'));
|
||||
|
||||
$list = is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : array();
|
||||
$children = $this->children();
|
||||
if(!empty($children)) $list = array_merge($list, $children);
|
||||
|
||||
if($grid)
|
||||
{
|
||||
foreach($list as $key => $item)
|
||||
{
|
||||
if($item instanceof formGroup) $list[$key] = new formRow($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The formBase widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
namespace zin;
|
||||
|
||||
/**
|
||||
* 基础表单(formBase)部件类,支持 Ajax 提交
|
||||
* The formBase widget class
|
||||
*/
|
||||
class formBase extends wg
|
||||
{
|
||||
protected static array $defineProps = array(
|
||||
'id?: string="$GID"', // ID,如果不指定则自动生成(使用 zin 部件 GID)。
|
||||
'method?: "get"|"post"="post"', // 表单提交方式。
|
||||
'url?: string', // 表单提交地址。
|
||||
'actions?: array', // 表单操作按钮,如果不指定则使用默认行为的 “保存” 和 “返回” 按钮。
|
||||
'actionsClass?: string', // 表单操作按钮栏类名。
|
||||
'target?: string="ajax"', // 表单提交目标,如果是 `'ajax'` 提交则为 ajax,在禅道中除非特殊目的,都使用 ajax 进行提交。
|
||||
'submitBtnText?: string', // 表单提交按钮文本,如果不指定则使用 `$lang->save` 的值。
|
||||
'cancelBtnText?: string', // 表单取消按钮文本,如果不指定则使用 `$lang->goback` 的值。
|
||||
'back?: string="APP"', // 表单返回行为
|
||||
'backUrl?: string', // 表单返回链接
|
||||
'ajax?:array' // Ajax 表单选项
|
||||
);
|
||||
|
||||
protected static array $defineBlocks = array(
|
||||
'actions' => array('toolbar')
|
||||
);
|
||||
|
||||
protected function created()
|
||||
{
|
||||
if($this->prop('actions') !== null) return;
|
||||
|
||||
$actions = isAjaxRequest('modal') ? array('submit') : array('submit', 'cancel');
|
||||
$this->setDefaultProps(array('actions' => $actions));
|
||||
}
|
||||
|
||||
protected function buildActions(): wg|null
|
||||
{
|
||||
if($this->hasBlock('actions')) return $this->block('actions');
|
||||
|
||||
$actions = $this->prop('actions');
|
||||
if(empty($actions)) return null;
|
||||
|
||||
global $lang;
|
||||
$submitBtnText = $this->prop('submitBtnText');
|
||||
$cancelBtnText = $this->prop('cancelBtnText');
|
||||
$backUrl = $this->prop('backUrl');
|
||||
$back = $this->prop('back');
|
||||
if(empty($submitBtnText)) $submitBtnText = $lang->save;
|
||||
if(empty($cancelBtnText)) $cancelBtnText = $lang->goback;
|
||||
foreach($actions as $key => $action)
|
||||
{
|
||||
if($action === 'submit') $actions[$key] = array('text' => $submitBtnText, 'btnType' => 'submit', 'type' => 'primary');
|
||||
elseif($action === 'cancel') $actions[$key] = array('text' => $cancelBtnText, 'url' => $backUrl, 'back' => $back);
|
||||
elseif(is_string($action)) $actions[$key] = array('text' => $action);
|
||||
}
|
||||
|
||||
return toolbar
|
||||
(
|
||||
set::class('form-actions', $this->prop('actionsClass')),
|
||||
set::items($actions)
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildContent(): array|wg
|
||||
{
|
||||
return $this->children();
|
||||
}
|
||||
|
||||
protected function buildProps(): array
|
||||
{
|
||||
list($url, $target, $method, $id) = $this->prop(array('url', 'target', 'method', 'id'));
|
||||
return array(
|
||||
set::class('form load-indicator'),
|
||||
$target === 'ajax' ? set::class('form-ajax') : null,
|
||||
set(array(
|
||||
'id' => $id,
|
||||
'action' => empty($url) ? $_SERVER['REQUEST_URI'] : $url,
|
||||
'target' => $target === 'ajax' ? null: $target,
|
||||
'method' => $method
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildAfter(): array
|
||||
{
|
||||
$after = parent::buildAfter();
|
||||
if($this->prop('target') === 'ajax')
|
||||
{
|
||||
$after[] = zui::ajaxForm(set::_to('#' . $this->id()), set($this->prop('ajax')));
|
||||
}
|
||||
return $after;
|
||||
}
|
||||
|
||||
protected function build(): wg
|
||||
{
|
||||
return h::form
|
||||
(
|
||||
$this->buildProps(),
|
||||
set($this->getRestProps()),
|
||||
$this->buildContent(),
|
||||
$this->buildActions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The formBatch widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'formbatchitem' . DS . 'v1.php';
|
||||
require_once dirname(__DIR__) . DS . 'formbase' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 批量编辑表单(formBatch)部件类,支持 Ajax 提交。
|
||||
* The batch operate form widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class formBatch extends formBase
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'items?: array[]', // 使用一个列定义对象数组来定义批量表单项。
|
||||
'minRows?: int', // 最小显示的行数目。
|
||||
'maxRows?: int', // 最多显示的行数目。
|
||||
'data?: array[]', // 初始化行数据。
|
||||
'mode?: string', // 批量操作模式,可以为 `'add'`(批量添加) 或 `'edit'`(批量编辑)。
|
||||
'actionsText?: string', // 操作列头部文本,如果不指定则使用 `$lang->actions` 的值。
|
||||
'idKey?: string', // 用于从行数据获取 ID 的属性名。
|
||||
'addRowIcon?: string|false', // 添加行的图标,如果设置为 `false` 则不显示图标
|
||||
'deleteRowIcon?: string|false', // 删除行的图标,如果设置为 `false` 则不显示图标
|
||||
'onRenderRow?: function', // 渲染行时的回调函数。
|
||||
'onRenderRowCol?: function' // 渲染列时的回调函数。
|
||||
);
|
||||
|
||||
/**
|
||||
* Define default properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defaultProps = array(
|
||||
'minRows' => 1,
|
||||
'maxRows' => 100,
|
||||
'mode' => 'add'
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle building inner items.
|
||||
*
|
||||
* @param wg|array wg
|
||||
* @access public
|
||||
* @return wg
|
||||
*/
|
||||
public function onBuildItem(wg|array $item): wg
|
||||
{
|
||||
if($item instanceof formBatchItem) return $item;
|
||||
|
||||
if(!($item instanceof item))
|
||||
{
|
||||
if(!is_array($item)) return $item;
|
||||
$item = item(set($item));
|
||||
}
|
||||
|
||||
return new formBatchItem(inherit($item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build batch form content.
|
||||
*
|
||||
* @access protected
|
||||
* @return array|wg
|
||||
*/
|
||||
protected function buildContent(): array|wg
|
||||
{
|
||||
$items = array_merge($this->children(), $this->prop('items', array()));
|
||||
$templateItems = array();
|
||||
$headItems = array();
|
||||
$otherItems = array();
|
||||
|
||||
foreach($items as $item)
|
||||
{
|
||||
if($item instanceof item || is_array($item)) $item = $this->onBuildItem($item);
|
||||
if($item instanceof formBatchItem)
|
||||
{
|
||||
list($headItem, $templateItem) = $item->build();
|
||||
$headItems[] = $headItem;
|
||||
$templateItems[] = $templateItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
$otherItems[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->prop('mode') === 'add')
|
||||
{
|
||||
$actionsText = $this->prop('actionsText');
|
||||
if($actionsText === null) $actionsText = data('lang.actions');
|
||||
$headItems[] = h::th
|
||||
(
|
||||
set('data-name', 'ACTIONS'),
|
||||
setClass('form-batch-head'),
|
||||
span(setClass('form-label form-batch-label'), $actionsText)
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
div
|
||||
(
|
||||
setClass('form-batch-container'),
|
||||
h::table
|
||||
(
|
||||
setClass('table form-batch-table'),
|
||||
h::thead(h::tr($headItems)),
|
||||
h::tbody(),
|
||||
)
|
||||
),
|
||||
template(setClass('form-batch-template'), h::tr($templateItems)),
|
||||
$otherItems
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build batch form props.
|
||||
*
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function buildProps(): array
|
||||
{
|
||||
$props = parent::buildProps();
|
||||
list($mode, $minRows, $maxRows) = $this->prop(array('mode', 'minRows', 'maxRows'));
|
||||
|
||||
$props[] = setClass('form-batch');
|
||||
$props[] = set('data-mode', $mode);
|
||||
$props[] = set('data-min-rows', $minRows);
|
||||
$props[] = set('data-max-rows', $maxRows);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build content after current widget.
|
||||
*/
|
||||
protected function buildAfter(): array
|
||||
{
|
||||
$after = parent::buildAfter();
|
||||
|
||||
$after[] = zui::batchForm
|
||||
(
|
||||
set::_to('#' . $this->id()),
|
||||
set($this->props->pick(array('minRows', 'maxRows', 'data', 'mode', 'idKey', 'onRenderRow', 'onRenderRowCol', 'addRowIcon', 'deleteRowIcon')))
|
||||
);
|
||||
return $after;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* The formBatch widget class file of zin module of ZenTaoPMS.
|
||||
*
|
||||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
||||
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
* @author sunhao<sunhao@easycorp.ltd>
|
||||
* @package zin
|
||||
* @link http://www.zentao.net
|
||||
*/
|
||||
|
||||
namespace zin;
|
||||
|
||||
require_once dirname(__DIR__) . DS . 'control' . DS . 'v1.php';
|
||||
|
||||
/**
|
||||
* 批量编辑表单项(formBatchItem)部件类。
|
||||
* The batch edit form item widget class.
|
||||
*
|
||||
* @author Hao Sun
|
||||
*/
|
||||
class formBatchItem extends wg
|
||||
{
|
||||
/**
|
||||
* Define widget properties.
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
protected static array $defineProps = array(
|
||||
'name: string', // 表单项名称,无需包含 `[]`。
|
||||
'label: string|bool', // 列标题。
|
||||
'labelClass?: string', // 列标题类名。
|
||||
'labelProps?: string', // 列标题属性,例如 `array('data-toggle' => 'tooltip', 'data-title' 。=> 'This is a tip')`
|
||||
'required?:bool|string="auto"', // 是否必填,如果设置为 `"auto"`,则自动从当前模块 config 中查询。
|
||||
'control?: array|string|false', // 控件类型或控件配置。
|
||||
'width?: number|string', // 列宽度,如果设置为 `"auto"` 则自动填充剩余宽度。
|
||||
'minWidth?: number|string', // 列最小宽度。
|
||||
'value?: string|array', // 默认值。
|
||||
'disabled?: bool', // 是否禁用。
|
||||
'items?: array', // 选项,当控件类型为下拉菜单时使用此属性指定下拉菜单项。
|
||||
'placeholder?: string', // 占位文本。
|
||||
'tip?: string', // 显示在列标题上的提示文本。
|
||||
'tipClass?: string', // 列标题上的提示触发按钮类名。
|
||||
'tipIcon?: string="info-sign"', // 列标题上的提示触发按钮图标。
|
||||
'tipProps?: string', // 列标题上的提示触发按钮其他属性。
|
||||
'ditto?: bool', // 是否显示同上按钮。
|
||||
'defaultDitto?:string="on"', // 同上按钮的默认值。
|
||||
'hidden?: bool=false', // 是否隐藏
|
||||
'readonly?: bool=false', // 是否只读
|
||||
);
|
||||
|
||||
/**
|
||||
* Define default properties.
|
||||
*
|
||||
* @access protected
|
||||
*/
|
||||
protected function build(): array
|
||||
{
|
||||
list($name, $label, $labelClass, $labelProps, $required, $tip, $tipClass, $tipProps, $tipIcon, $control, $width, $strong, $value, $disabled, $items, $placeholder, $ditto, $defaultDitto, $hidden, $readonly, $multiple) = $this->prop(array('name', 'label', 'labelClass', 'labelProps', 'required', 'tip', 'tipClass', 'tipProps', 'tipIcon', 'control', 'width', 'strong', 'value', 'disabled', 'items', 'placeholder', 'ditto', 'defaultDitto', 'hidden', 'readonly', 'multiple'));
|
||||
|
||||
if($required === 'auto') $required = isFieldRequired($name);
|
||||
|
||||
if($control !== false)
|
||||
{
|
||||
if(is_string($control)) $control = array('type' => $control, 'name' => $name);
|
||||
else if(empty($control)) $control = array();
|
||||
|
||||
if(!isset($control['type'])) $control['type'] = 'text';
|
||||
if($required !== null) $control['required'] = $required;
|
||||
if($name !== null) $control['name'] = $name;
|
||||
if($value !== null) $control['value'] = $value;
|
||||
if($disabled !== null) $control['disabled'] = $disabled;
|
||||
if($multiple !== null) $control['multiple'] = $multiple;
|
||||
if($items !== null) $control['items'] = $items;
|
||||
if($placeholder !== null) $control['placeholder'] = $placeholder;
|
||||
if($readonly !== null) $control['readonly'] = $readonly;
|
||||
}
|
||||
|
||||
$asIndex = $control['type'] === 'index';
|
||||
if($asIndex) $control['type'] = 'static';
|
||||
|
||||
return array(
|
||||
h::th
|
||||
(
|
||||
setClass('form-batch-head'),
|
||||
$hidden ? setClass('hidden') : null,
|
||||
zui::width($width),
|
||||
set('data-required', $required),
|
||||
set('data-ditto', $ditto),
|
||||
set('data-name', $name),
|
||||
$ditto ? set('data-default-ditto', $defaultDitto) : null,
|
||||
$asIndex ? set('data-index', $asIndex) : null,
|
||||
set($this->getRestProps()),
|
||||
span
|
||||
(
|
||||
set::class('form-label form-batch-label', $labelClass, $strong ? 'font-bold' : null, $required ? 'required' : null),
|
||||
set($labelProps),
|
||||
$label
|
||||
),
|
||||
empty($tip) ? null : new btn
|
||||
(
|
||||
set::class('form-batch-tip state text-gray', $tipClass),
|
||||
set::size('sm'),
|
||||
set::type('ghost'),
|
||||
toggle('tooltip', array('title' => $tip)),
|
||||
set($tipProps),
|
||||
set::icon($tipIcon)
|
||||
)
|
||||
),
|
||||
h::td
|
||||
(
|
||||
setClass('form-batch-control'),
|
||||
$hidden ? setClass('hidden') : null,
|
||||
set('data-name', $name),
|
||||
empty($control) ? null : new control(set($control)),
|
||||
$this->children()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user