+ add zin and zui3 files.

This commit is contained in:
sunhao
2023-07-25 17:42:27 +08:00
parent b92acecc1a
commit e97ab9686b
234 changed files with 82310 additions and 178 deletions
+100 -5
View File
@@ -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.
+105 -4
View File
@@ -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.
+43 -17
View File
@@ -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();
}
}
}
+6 -5
View File
@@ -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");
+260
View File
@@ -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;
}
+29 -20
View File
@@ -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', '');
+107
View File
@@ -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());
}
}
+25
View File
@@ -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/');
}
+169
View File
@@ -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]);
}
}
+40
View File
@@ -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());
}
+53
View File
@@ -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);
}
+66
View File
@@ -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);
}
+395
View File
@@ -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;
}
}
+300
View File
@@ -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');
}
+47
View File
@@ -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());}
+33
View File
@@ -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());
}
+24
View File
@@ -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);
}
}
+27
View File
@@ -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);
}
}
+284
View File
@@ -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 &quot;Hello&quot;!" 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;
}
}
+23
View File
@@ -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}}');
}
}
+60
View File
@@ -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;
}
+165
View File
@@ -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;
}
+30
View File
@@ -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));
}
}
+23
View File
@@ -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);
}
}
+752
View File
@@ -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;
}
}
+305
View File
@@ -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();
}
+76
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+131
View File
@@ -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;
}
+314
View File
@@ -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;
}
}
+84
View File
@@ -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;
}
}
+288
View File
@@ -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);
}
}
+45
View File
@@ -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);};
+38
View 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;
}
+20
View File
@@ -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;
}
+126
View File
@@ -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();
}
}
+174
View File
@@ -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";
}
}
+139
View File
@@ -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()
);
}
}
+297
View File
@@ -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
);
}
}
+101
View File
@@ -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;
}
}
+72
View File
@@ -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');
}
+39
View File
@@ -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'),
),
)
);
}
}
+77
View File
@@ -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;
}
}
+105
View File
@@ -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
);
}
}
+44
View File
@@ -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()
);
}
}
+41
View File
@@ -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));
}
}
+49
View File
@@ -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()
);
}
}
+16
View File
@@ -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()
);
}
}
+75
View File
@@ -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()
);
}
}
+1
View File
@@ -0,0 +1 @@
.checkbox-list {border-left: 1px solid var(--color-gray-400);}
+41
View File
@@ -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;
}
}
}
+67
View File
@@ -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(),
);
}
}
+66
View File
@@ -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()
);
}
}
+26
View File
@@ -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()
);
}
}
+39
View File
@@ -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
)
);
}
}
+83
View File
@@ -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(),
);
}
}
+42
View File
@@ -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;
}
}
+35
View File
@@ -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),
)
);
}
}
+40
View File
@@ -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)
)
)
);
}
}
+167
View File
@@ -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));
}
}
+60
View File
@@ -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')));
}
}
+85
View File
@@ -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(),
);
}
}
+12
View File
@@ -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'
);
}
+1
View File
@@ -0,0 +1 @@
.detail-body > .form-actions:last-child {margin-top: 0;}
+9
View File
@@ -0,0 +1,9 @@
$(() =>
{
const $formActions = $('form.detail-body + .form-actions');
if($formActions.length)
{
$detailBody = $formActions.prev();
$detailBody.append($formActions);
}
});
+82
View File
@@ -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,
),
)
);
}
}
+51
View File
@@ -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
);
}
}
+6
View File
@@ -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;}
+21
View File
@@ -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()
);
}
}
+19
View File
@@ -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;}
+61
View File
@@ -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);
}
+416
View File
@@ -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(),
);
}
}
+1
View File
@@ -0,0 +1 @@
.hold {border: 1px dashed #ccc; box-sizing: border-box;}
+123
View File
@@ -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;
}
}
+165
View File
@@ -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);
}
}
+5
View File
@@ -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));}
+104
View File
@@ -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 methodobjectIDextra 等参数则可以忽略。
'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())
);
}
}
+5
View File
@@ -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);}
+184
View File
@@ -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));
}
}
+27
View File
@@ -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}
+137
View File
@@ -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;
}
}
+34
View File
@@ -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)))"));
}
}
+12
View File
@@ -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;}
+69
View File
@@ -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'])
),
);
}
}
+1
View File
@@ -0,0 +1 @@
.entity-label .label {box-shadow: none; border: 1px solid var(--color-slate-300); color: var(--color-slate-700); background: none;}
+105
View File
@@ -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
);
}
}
+148
View File
@@ -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')
);
}
}
+13
View File
@@ -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'
);
}
+12
View 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}
+127
View File
@@ -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
})
}
+89
View File
@@ -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;
}
}
+2
View File
@@ -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;}
+24
View File
@@ -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);
});
+44
View File
@@ -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,
);
}
}
+4
View File
@@ -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;}
+99
View File
@@ -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,
);
}
}
+88
View File
@@ -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;
}
}
+114
View File
@@ -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(),
);
}
}
+167
View File
@@ -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;
}
}
+122
View File
@@ -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