diff --git a/lib/zin/core/context.class.php b/lib/zin/core/context.class.php index e9f8a719dc..07f2f188d7 100644 --- a/lib/zin/core/context.class.php +++ b/lib/zin/core/context.class.php @@ -135,7 +135,7 @@ class context extends \zin\utils\dataset */ public static function current(): context { - if(empty(static::$map)) static::$map['current'] = new context(null); + if(empty(static::$map)) static::$map['current'] = new context(); return static::$map['current']; } diff --git a/lib/zin/core/data.func.php b/lib/zin/core/data.func.php index 14fd179a70..fb8cf0d02a 100644 --- a/lib/zin/core/data.func.php +++ b/lib/zin/core/data.func.php @@ -37,8 +37,10 @@ function getPageData($name) return zin::getData($name); } -function data(...$args) +function data() { + $args = func_get_args(); + if(count($args) >= 2) return setPageData($args[0], $args[1]); return getPageData($args[0]); } diff --git a/lib/zin/core/dom.class.php b/lib/zin/core/dom.class.php index 8218c53135..588eee3492 100644 --- a/lib/zin/core/dom.class.php +++ b/lib/zin/core/dom.class.php @@ -267,7 +267,7 @@ class dom if($item instanceof dom) { - $json = $item->wg->toJsonData(); + $json = $item->wg->toJSON(); if(!empty($item->dataGetters)) { $output = array(); diff --git a/lib/zin/core/h.class.php b/lib/zin/core/h.class.php index 44f440d755..ba9fd8f012 100644 --- a/lib/zin/core/h.class.php +++ b/lib/zin/core/h.class.php @@ -121,8 +121,11 @@ class h extends wg return static::create('input', set('type', 'text'), func_get_args()); } - public static function formHidden($name, $value, ...$args) + public static function formHidden(/* $name, $value, ...$args */) { + $args = func_get_args(); + $name = array_shift($args); + $value = array_shift($args); return static::create('input', set('type', 'hidden'), set::name($name), set::value($value), $args); } @@ -146,10 +149,11 @@ class h extends wg return static::create('input', set('type', 'file'), func_get_args()); } - public static function textarea(...$args) + public static function textarea(/* ...$args */) { + $args = func_get_args(); list($code, $args) = h::splitRawCode($args); - return static::create('textarea', $code, ...$args); + return static::create('textarea', $code, $args); } /** @@ -164,18 +168,26 @@ class h extends wg return html(""); } - public static function importJs($src, ...$args) + public static function importJs(/* $src, ...$args */) { - return static::create('script', set('src', $src), ...$args); + $args = func_get_args(); + $src = array_shift($args); + return static::create('script', set('src', $src), $args); } - public static function importCss($src, ...$args) + public static function importCss(/* $src, ...$args */) { - return static::create('link', set('rel', 'stylesheet'), set('href', $src), ...$args); + $args = func_get_args(); + $src = array_shift($args); + return static::create('link', set('rel', 'stylesheet'), set('href', $src), $args); } - public static function import($file, $type = null, ...$args) + public static function import(/* $file, $type = null, ...$args */) { + $args = array_merge(func_get_args(), array(null, null)); + $file = array_shift($args); + $type = array_shift($args); + if(is_array($file)) { $children = array(); @@ -186,40 +198,46 @@ class h extends wg 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); + 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) + public static function css(/* ...$args */) { - list($code, $args) = h::splitRawCode($args); + list($code, $args) = h::splitRawCode(func_get_args()); if(empty($code)) return null; - return static::create('style', html(implode("\n", $code)), ...$args); + return static::create('style', html(implode("\n", $code)), $args); } - public static function globalJS(...$args) + public static function globalJS(/* ...$args */) { - list($code, $args) = h::splitRawCode($args); + list($code, $args) = h::splitRawCode(func_get_args()); if(empty($code)) return null; - return static::create('script', html(implode("\n", $code)), ...$args); + return static::create('script', html(implode("\n", $code)), $args); } - public static function js(...$args) + public static function js(/* ...$args */) { - list($code, $args) = h::splitRawCode($args); + list($code, $args) = h::splitRawCode(func_get_args()); if(empty($code)) return null; - return static::create('script', html(h::createJsScopeCode($code)), ...$args); + return static::create('script', html(h::createJsScopeCode($code)), $args); } - public static function jsVar($name, $value, ...$directives) + public static function jsVar(/* $name, $value, ...$args */) { - return static::js(static::createJsVarCode($name, $value), ...$directives); + $args = func_get_args(); + $name = array_shift($args); + $value = array_shift($args); + return static::js(static::createJsVarCode($name, $value), $args); } - public static function jsCall($funcName, ...$args) + public static function jsCall(/* $funcName, ...$args */) { + $args = func_get_args(); + $funcName = array_shift($args); + $funcArgs = []; $directives = []; foreach($args as $arg) @@ -228,7 +246,7 @@ class h extends wg else $funcArgs[] = $arg; } $code = static::createJsCallCode($funcName, $funcArgs); - return static::js($code, ...$directives); + return static::js($code, $directives); } public static function createJsCallCode($func, $args) diff --git a/lib/zin/core/portal.class.php b/lib/zin/core/portal.class.php deleted file mode 100644 index 11cefe2157..0000000000 --- a/lib/zin/core/portal.class.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @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); - } -} diff --git a/lib/zin/core/props.class.php b/lib/zin/core/props.class.php index bd8478601a..184847f0b3 100644 --- a/lib/zin/core/props.class.php +++ b/lib/zin/core/props.class.php @@ -48,10 +48,10 @@ class props extends \zin\utils\dataset * @access public * @param array $props - Properties list array */ - public function __construct(?array $props = null) + public function __construct(array $props = array()) { - $this->style = new \zin\utils\style(); - $this->class = new \zin\utils\classlist(); + $this->style = style::new(); + $this->class = classlist::new(); parent::__construct($props); } @@ -208,11 +208,11 @@ class props extends \zin\utils\dataset return implode(' ', $pairs); } - public function toJsonData(bool $skipEvents = false): array + public function toJSON(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(!empty($this->class->toJSON())) $data['class'] = $this->class->toStr(); if($skipEvents) { @@ -228,7 +228,7 @@ class props extends \zin\utils\dataset { if(is_string($skipProps)) $skipProps = explode(',', $skipProps); - $data = $this->toJsonData(); + $data = $this->toJSON(); foreach($data as $name => $value) { if($value === null || $name[0] === '@' || in_array($name, $skipProps)) unset($data[$name]); @@ -242,7 +242,7 @@ class props extends \zin\utils\dataset { if(is_string($firstListProps)) $firstListProps = explode(',', $firstListProps); - $data = $this->toJsonData(); + $data = $this->toJSON(); $firstList = array(); $restList = array(); foreach($data as $name => $value) @@ -259,7 +259,7 @@ class props extends \zin\utils\dataset { if(is_string($pickProps)) $pickProps = explode(',', $pickProps); - $data = $this->toJsonData(); + $data = $this->toJSON(); foreach($data as $name => $value) { if($value === null || !in_array($name, $pickProps)) unset($data[$name]); @@ -277,8 +277,8 @@ class props extends \zin\utils\dataset public function clone(): props { $props = new props($this->data); - $props->style = clone $this->style; - $props->class = clone $this->class; + $props->style = clone $this->style; + $props->class = clone $this->class; return $props; } } diff --git a/lib/zin/core/set.class.php b/lib/zin/core/set.class.php index 80b0ff69c8..e65ad61e2a 100644 --- a/lib/zin/core/set.class.php +++ b/lib/zin/core/set.class.php @@ -23,8 +23,8 @@ class set return directive('prop', array($prop => $value)); } - public static function class(...$args) + public static function class(/* ...$args */) { - return directive('prop', array('class' => $args)); + return directive('prop', array('class' => func_get_args())); } } diff --git a/lib/zin/core/wg.class.php b/lib/zin/core/wg.class.php index d8e814c3e1..3255ddb165 100644 --- a/lib/zin/core/wg.class.php +++ b/lib/zin/core/wg.class.php @@ -73,7 +73,7 @@ class wg public function __debugInfo(): array { - return $this->toJsonData(); + return $this->toJSON(); } public function isDomElement(): bool @@ -429,7 +429,7 @@ class wg */ public function setProp(props|array|string $prop, mixed $value = null) { - if($prop instanceof props) $prop = $prop->toJsonData(); + if($prop instanceof props) $prop = $prop->toJSON(); if(is_array($prop)) { @@ -498,11 +498,11 @@ class wg return $this->prop('id'); } - public function toJsonData(): array + public function toJSON(): array { $data = array(); $data['gid'] = $this->gid; - $data['props'] = $this->props->toJsonData(); + $data['props'] = $this->props->toJSON(); $data['type'] = $this->type(); if(str_starts_with($data['type'], 'zin\\')) $data['type'] = substr($data['type'], 4); @@ -512,9 +512,9 @@ class wg { foreach($value as $index => $child) { - if($child instanceof wg || (is_object($child) && method_exists($child, 'toJsonData'))) + if($child instanceof wg || (is_object($child) && method_exists($child, 'toJSON'))) { - $value[$index] = $child->toJsonData(); + $value[$index] = $child->toJSON(); } elseif(isDirective($child, 'html')) { diff --git a/lib/zin/func.php b/lib/zin/func.php index 7fe5866774..70ac74aa2e 100644 --- a/lib/zin/func.php +++ b/lib/zin/func.php @@ -658,6 +658,14 @@ function cell(): cell return createWg('cell', func_get_args()); } +/** + * Divider widget. + */ +function divider(): divider +{ + return createWg('divider', func_get_args()); +} + /** * Action item widget. * @@ -1056,14 +1064,6 @@ function toolbar(): toolbar return createWg('toolbar', func_get_args()); } -/** - * Zentao search form widget. - */ -function searchForm(): searchForm -{ - return createWg('searchForm', func_get_args()); -} - /** * Zentao search toggle widget. * @@ -1627,6 +1627,15 @@ function overviewBlock(): overviewBlock return createWg('overviewBlock', func_get_args()); } +/** + * Statistic block widget. + * + */ +function statisticBlock(): statisticBlock +{ + return createWg('statisticBlock', func_get_args()); +} + /** * Picker widget. */ @@ -1689,3 +1698,62 @@ function tableChart(): tableChart { return createWg('tableChart', func_get_args()); } + +/** + * Password widget. + */ +function password(): password +{ + return createWg('password', func_get_args()); +} + +/** + * Mindmap widget. + */ +function mindmap(): mindmap +{ + return createWg('mindmap', func_get_args()); +} + +/** + * Image cutter widget. + */ +function imgCutter(): imgCutter +{ + return createWg('imgCutter', func_get_args()); +} + +/** + * Next step modal widget. + * + * string tip + * array items + */ +function modalNextStep(): modalNextStep +{ + return createWg('modalNextStep', func_get_args()); +} + +/** + * Navigator widget. + */ +function navigator(): navigator +{ + return createWg('navigator', func_get_args()); +} + +/** + * Gantt widget. + */ +function gantt(): gantt +{ + return createWg('gantt', func_get_args()); +} + +/** + * road map widget. + */ +function roadMap(): roadMap +{ + return createWg('roadmap', func_get_args()); +} diff --git a/lib/zin/utils/classlist.class.php b/lib/zin/utils/classlist.class.php index 4332cf9eff..1c765d979e 100644 --- a/lib/zin/utils/classlist.class.php +++ b/lib/zin/utils/classlist.class.php @@ -20,10 +20,32 @@ class classlist /** * Store classname list, key => value * - * @access public + * @access private * @var array */ - public $list = array(); + private array $list = array(); + + /** + * Create an classlist instance + * + * @param string|array|null $names - A string or a class name list + * @return classlist + */ + public static function new(string|array|null $names = array()): classlist + { + return new classlist($names); + } + + /** + * Stringify class list + * + * @param string|array $names - A string or a class name list + * @return string + */ + public static function str(string|array $names): string + { + return classlist::new($names)->toStr(); + } /** * Create classname instance @@ -43,58 +65,18 @@ class classlist * @access public * @return string */ - public function __toString() + public function __toString(): string { 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 = classlist::new(); * $classlist->set('btn primary rounded'); * * // Set multiple classnames by string list @@ -104,11 +86,11 @@ class classlist * $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 + * @param string|array|null $list - A string or a class name list + * @param bool $reset * @return classlist */ - public function set($list, $reset = false) + public function set(string|array|null $list, bool $reset = false): classlist { if(is_string($list)) $list = explode(' ', $list); @@ -151,7 +133,7 @@ class classlist * * Example: * - * $classlist = new classlist(); + * $classlist = classlist::new(); * $classlist->add('btn primary rounded'); * * // Add multiple classnames by string list @@ -171,7 +153,7 @@ class classlist * * Example: * - * $classlist = new classlist('btn primary rounded'); + * $classlist = classlist::new('btn primary rounded'); * $classlist->remove('btn primary'); * * // Add multiple classnames by string list @@ -181,7 +163,7 @@ class classlist * @param array|string $list - classname string joined by space or string array * @return classlist */ - public function remove($list) + public function remove(array|string $list): classlist { if(is_string($list)) $list = explode(' ', $list); @@ -201,7 +183,7 @@ class classlist * * Example: * - * $classlist = new classlist('btn'); + * $classlist = classlist::new('btn'); * $classlist->toggle('btn'); // class list is "" * * // Toggle class name by flag @@ -211,7 +193,7 @@ class classlist * @param string $name - classname string * @return classlist */ - public function toggle($name, $toggle = null) + public function toggle(string $name, bool|null $toggle = null): classlist { $name = trim($name); if(strlen($name)) @@ -227,13 +209,13 @@ class classlist * * Example: * - * $classlist = new classlist('btn primary rounded'); + * $classlist = classlist::new('btn primary rounded'); * echo $classlist->has('btn'); // Output true * * // Check multiple names * echo $classlist->has('btn primary'); // Output true */ - public function has($list) + public function has(array|string $list): bool { if(is_string($list)) $list = explode(' ', $list); @@ -280,34 +262,12 @@ class classlist * @access public * @return int */ - public function count() + public function count(): int { 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() + public function toJSON(): array { return $this->list; } diff --git a/lib/zin/utils/data.class.php b/lib/zin/utils/data.class.php deleted file mode 100644 index 1f34a87a83..0000000000 --- a/lib/zin/utils/data.class.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @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; - } -} diff --git a/lib/zin/utils/dataset.class.php b/lib/zin/utils/dataset.class.php index 5658f9189e..679813f7fa 100644 --- a/lib/zin/utils/dataset.class.php +++ b/lib/zin/utils/dataset.class.php @@ -21,9 +21,9 @@ class dataset * Store dataset properties list in an array * * @var array - * @access public + * @access protected */ - public array $data = array(); + protected array $data = array(); /** * Create an instance, the initialed data can be passed @@ -31,60 +31,11 @@ class dataset * @access public * @param array $data - Properties list array */ - public function __construct(?array $data = null) + public function __construct(array $data = array()) { 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 * @@ -96,43 +47,6 @@ class dataset 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 * @@ -179,10 +93,10 @@ class dataset */ public function toStr(): string { - return json_encode($this->toJsonData()); + return json_encode($this->toJSON()); } - public function toJsonData(): array + public function toJSON(): array { return $this->data; } @@ -233,12 +147,6 @@ class dataset 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 * diff --git a/lib/zin/utils/debug.func.php b/lib/zin/utils/debug.func.php index eedc48a7d3..68b33ab926 100644 --- a/lib/zin/utils/debug.func.php +++ b/lib/zin/utils/debug.func.php @@ -16,7 +16,7 @@ $logs = array(); function log($type, $msg = null, $file) { - global $config; + global $config, $logs; if(!$config->debug) return; diff --git a/lib/zin/utils/flat.func.php b/lib/zin/utils/flat.func.php index f597fbf17b..5461325080 100644 --- a/lib/zin/utils/flat.func.php +++ b/lib/zin/utils/flat.func.php @@ -2,14 +2,14 @@ declare(strict_types=1); namespace zin\utils; -function flat($array, $prefix = '', $separator = '.') +function flat(array $array, string $prefix = '') { $result = array(); foreach($array as $key => $value) { if(is_array($value)) { - $result = array_merge($result, flat($value, $prefix . $key . $separator)); + $result = array_merge($result, flat($value, "{$prefix}{$key}.")); } else { diff --git a/lib/zin/utils/style.class.php b/lib/zin/utils/style.class.php index 19b8b7607c..709790902d 100644 --- a/lib/zin/utils/style.class.php +++ b/lib/zin/utils/style.class.php @@ -43,6 +43,41 @@ require_once __DIR__ . DS . 'dataset.class.php'; */ class style extends dataset { + /** + * Create an instance + * + * @param array $style - CSS style list + * @return style + */ + public static function new(array $style = array()): style + { + return new style($style); + } + + /** + * Create css from style list + * + * @access public + * @param array $style - CSS style list + * @return string + */ + public static function str(array $style): string + { + return (new style($style))->toStr(); + } + + /** + * 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"; + } + /** * 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 @@ -70,9 +105,9 @@ class style extends dataset * $style->cssVar('text-color', ''); * * @access public - * @param array|string $name - Variable name or variables list - * @param mixed $value - Property value - * @return mixed + * @param array|string $name - Variable name or variables list + * @param string|null $value - Property value + * @return style|array|string */ public function cssVar(array|string $name = '', ?string $value = null): style|array|string { @@ -112,17 +147,6 @@ class style extends dataset * @return string */ public function toStr(): string - { - return $this->toCss(); - } - - /** - * Convert style to css string - * - * @access public - * @return string - */ - public function toCss(): string { $pairs = array(); @@ -136,39 +160,4 @@ class style extends dataset 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"; - } } diff --git a/lib/zin/wg/avatar/v1.php b/lib/zin/wg/avatar/v1.php index 41a534f294..c91e8823c1 100644 --- a/lib/zin/wg/avatar/v1.php +++ b/lib/zin/wg/avatar/v1.php @@ -49,7 +49,7 @@ class avatar extends wg $this->finalStyle->background = $this->prop('background'); $this->finalStyle->color = $this->prop('foreColor'); - foreach($this->props->style->data as $attr => $val) $this->finalStyle->{$attr} = $val; + foreach($this->props->style->toJSON() as $attr => $val) $this->finalStyle->{$attr} = $val; /* Init avatar size. */ $this->initSize(); @@ -263,6 +263,7 @@ class avatar extends wg { $src = $this->prop('src'); $text = $this->prop('text'); + $code = $this->prop('code'); /* With avatar. */ if($src) @@ -272,8 +273,9 @@ class avatar extends wg return h::img ( setClass('avatar-img'), - set('src', $src ), - set('alt', $text) + set('src', $src), + set('alt', $text), + set('data-code', $code), ); } diff --git a/lib/zin/wg/backbtn/v1.php b/lib/zin/wg/backbtn/v1.php index 202b88dbb8..bb535c57cc 100644 --- a/lib/zin/wg/backbtn/v1.php +++ b/lib/zin/wg/backbtn/v1.php @@ -58,6 +58,7 @@ class backBtn extends btn 'build' => 'execution-build,build-view', 'projectbuild' => 'projectbuild-browse,projectbuild-view', 'mr' => 'mr-browse', + 'repo' => 'repo-log,repo-browse', 'compile' => 'compile-browse', 'store' => 'store-browse', 'space' => 'space-browse', diff --git a/lib/zin/wg/batchactions/js/v1.js b/lib/zin/wg/batchactions/js/v1.js index c83bd1e1e5..f85d2eca13 100644 --- a/lib/zin/wg/batchactions/js/v1.js +++ b/lib/zin/wg/batchactions/js/v1.js @@ -1,3 +1,13 @@ +/** + * Determines whether its argument represents a JavaScript number. + * @param {*} obj + * @returns bool + */ +function isNumeric(obj) +{ + return (!isNaN(obj) && typeof obj === 'number') || $.isNumeric(obj);; +} + /** * Add new item. * diff --git a/lib/zin/wg/blockpanel/v1.php b/lib/zin/wg/blockpanel/v1.php index 2322911cef..b26ddedb59 100644 --- a/lib/zin/wg/blockpanel/v1.php +++ b/lib/zin/wg/blockpanel/v1.php @@ -30,6 +30,7 @@ class blockPanel extends panel 'block?: object|array', // 区块对象。 'title?: string', // 标题。 'headingClass?: string="border-b"', // 标题栏类名。 + 'longBlock?: bool', // 是否为长区块。 'moreLink?: string' // 更多链接。 ); @@ -59,19 +60,24 @@ class blockPanel extends panel if(empty($this->prop('title'))) $props['title'] = empty($block) ? $lang->block->titleList[$name] : $block->title; + if($this->prop('longBlock') === null) $props['longBlock'] = data('longBlock'); + $this->setProp($props); } protected function buildProps(): array { $props = parent::buildProps(); - $name = $this->prop('name'); + $name = $this->prop('name'); if(!empty($name)) { $props[] = setData('block', $name); $props[] = setClass("block-{$name}"); $props[] = setID($this->prop('id')); } + + $props[] = setClass($this->prop('longBlock') ? 'is-long' : 'is-short'); + return $props; } } diff --git a/lib/zin/wg/btn/v1.php b/lib/zin/wg/btn/v1.php index e2ef19c5ce..a323546bdb 100644 --- a/lib/zin/wg/btn/v1.php +++ b/lib/zin/wg/btn/v1.php @@ -33,7 +33,7 @@ class btn extends wg protected function getProps() { - $url = $this->prop('url'); + $url = $this->prop('disabled') ? null : $this->prop('url'); $target = $this->prop('target'); $props = array_merge($this->getRestProps(), array('title' => $this->prop('hint'))); diff --git a/lib/zin/wg/center/v1.php b/lib/zin/wg/center/v1.php index 74255b09e9..fae71036c2 100644 --- a/lib/zin/wg/center/v1.php +++ b/lib/zin/wg/center/v1.php @@ -8,7 +8,7 @@ class center extends wg { return div ( - setClass("flex justify-center items-center"), + setClass("center"), set($this->getRestProps()), $this->children() ); diff --git a/lib/zin/wg/checklist/v1.php b/lib/zin/wg/checklist/v1.php index 8470b3735a..45657f6fce 100644 --- a/lib/zin/wg/checklist/v1.php +++ b/lib/zin/wg/checklist/v1.php @@ -12,7 +12,8 @@ class checkList extends wg 'name?: string', 'value?: string|array', 'items?: array', - 'inline?: bool' + 'inline?: bool', + 'disabled?: bool' ); public function getValueList() @@ -26,7 +27,7 @@ class checkList extends wg public function onBuildItem($item): checkbox { - if($item instanceof item) $item = $item->props->toJsonData(); + if($item instanceof item) $item = $item->props->toJSON(); if(!isset($item['checked'])) { @@ -34,15 +35,18 @@ class checkList extends wg $valueList = $this->getValueList(); $item['checked'] = in_array($value, $valueList); + $item['disabled'] = $this->prop('disabled'); } - $props = $this->props->pick(['primary', 'type', 'name']); + $props = $this->props->pick(['primary', 'type', 'name', 'disabled']); + if(!empty($props['name']) && !empty($item['value'])) $props['id'] = $props['name'] . $item['value']; + return new checkbox(set($props), set($item)); } protected function build(): wg { - list($items, $inline) = $this->prop(['items', 'inline']); + list($items, $inline, $disabled) = $this->prop(['items', 'inline', 'disabled']); if(!empty($items)) { @@ -59,6 +63,7 @@ class checkList extends wg ( setClass($inline ? 'check-list-inline' : 'check-list'), set($this->getRestProps()), + $disabled ? set('disabled', 'disabled') : '', $items, $this->children() ); diff --git a/lib/zin/wg/commentdialog/v1.php b/lib/zin/wg/commentdialog/v1.php index b28da9b9de..80a11e6bab 100644 --- a/lib/zin/wg/commentdialog/v1.php +++ b/lib/zin/wg/commentdialog/v1.php @@ -22,8 +22,8 @@ class commentDialog extends wg return modal ( - set::id('comment-dialog'), - set::title($title), + setID('comment-dialog'), + set::modalProps(array('title' => $title)), commentForm ( set::url($url), diff --git a/lib/zin/wg/control/v1.php b/lib/zin/wg/control/v1.php index cbe979298b..215b56a420 100644 --- a/lib/zin/wg/control/v1.php +++ b/lib/zin/wg/control/v1.php @@ -21,7 +21,7 @@ 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 属性 + 'name: string', // HTML name 属性 'id?: string', // HTML id 属性 'value?: string', // HTML value 属性 'placeholder?: string', // HTML placeholder 属性 diff --git a/lib/zin/wg/dashboard/v1.php b/lib/zin/wg/dashboard/v1.php index 105877f9b8..960f4413ff 100644 --- a/lib/zin/wg/dashboard/v1.php +++ b/lib/zin/wg/dashboard/v1.php @@ -39,7 +39,8 @@ class dashboard extends wg 'blockDefaultSize?: array', // 区块默认大小。 'blockSizeMap?: array', // 区块大小映射。 'blockMenu?: array', // 区块菜单。 - 'onLayoutChange?: function' // 布局变更事件。 + 'onLayoutChange?: function', // 布局变更事件。 + 'onClickMenu?: function' // 布局变更事件。 ); static $dashboardID = 0; @@ -55,6 +56,11 @@ class dashboard extends wg */ protected function build(): wg { - return zui::dashboard(set($this->props->skip(array('id'))), set('_id', $this->prop('id'))); + return zui::dashboard + ( + set($this->props->skip(array('id'))), + set('_id', $this->prop('id')), + set('_props', $this->getRestProps()) + ); } } diff --git a/lib/zin/wg/datetimepicker/v1.php b/lib/zin/wg/datetimepicker/v1.php index ec89ec91a4..917ab7170a 100644 --- a/lib/zin/wg/datetimepicker/v1.php +++ b/lib/zin/wg/datetimepicker/v1.php @@ -4,9 +4,71 @@ namespace zin; require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php'; -class datetimePicker extends input +class datetimePicker extends wg { - protected static array $defaultProps = array( - 'type' => 'datetime-local' + /** + * 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', // 选择框上的占位文本。 + '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', // 日期值无效时的回调函数。 + 'dateFormat?: string', // 日期格式,默认 yyyy-MM-dd。 + 'timeFormat?: string', // 时间格式,默认 hh:mm + 'joiner?: string' // 日期与时间的连接符,默认为单个空格 ); + + /** + * 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::datetimePicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set($props), + set::_props($restProps), + $this->children(), + ); + } } diff --git a/lib/zin/wg/divider/v1.php b/lib/zin/wg/divider/v1.php new file mode 100644 index 0000000000..a2e9e17d51 --- /dev/null +++ b/lib/zin/wg/divider/v1.php @@ -0,0 +1,16 @@ +getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/docmenu/css/v1.css b/lib/zin/wg/docmenu/css/v1.css index f6695f89cd..045133d8f6 100644 --- a/lib/zin/wg/docmenu/css/v1.css +++ b/lib/zin/wg/docmenu/css/v1.css @@ -1,15 +1,15 @@ -.module-menu {max-height: calc(100vh - 105px);} -.module-menu {padding: 0 0 8px;} +.module-menu {max-height: calc(100vh - 105px); 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 {padding-bottom: 0.5rem;} #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-actions .icon-ellipsis-v {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;} diff --git a/lib/zin/wg/docmenu/js/v1.js b/lib/zin/wg/docmenu/js/v1.js index a840b3e745..ddbb0006b8 100644 --- a/lib/zin/wg/docmenu/js/v1.js +++ b/lib/zin/wg/docmenu/js/v1.js @@ -4,19 +4,23 @@ window.saveModule = function() if(!name) return $(this).closest('.tree-item').remove(); const {id, type, lib, module} = $(this).data(); - const parentID = $(this).data('parent'); + const parentID = $(this).data('parent'); const $element = $(`div[data-id='${id}']`); $.ajaxSubmit({ - url: $.createLink('tree', 'ajaxCreateModule'), + url: $.createLink('tree', 'ajaxCreateModule'), data: { name : name, libID : lib, - parentID : parentID, + parentID : type == 'child' ? id : parentID, objectID : id, moduleType : module, isUpdate : false, createType : type, + }, + onSuccess: () => + { + $(this).val(''); } }); } @@ -26,7 +30,7 @@ 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'); + let parentID = ['docLib', 'apiLib'].includes(type) ? '0' : $element.data('parent'); if(addType == 'child') parentID = id; const level = addType == 'same' ? $element.data('level') : $element.data('level') + 1; @@ -57,5 +61,9 @@ window.addModule = function(id, addType) { $('#moduleName').trigger('focus'); document.getElementById("moduleName").addEventListener('blur', saveModule); + document.getElementById("moduleName").addEventListener('keydown', function(e) + { + if(e.keyCode == 13) saveModule.call(this); + }); }, 1); } diff --git a/lib/zin/wg/docmenu/v1.php b/lib/zin/wg/docmenu/v1.php index 8e61f4d53f..3d860c7eb5 100644 --- a/lib/zin/wg/docmenu/v1.php +++ b/lib/zin/wg/docmenu/v1.php @@ -12,7 +12,6 @@ class docMenu extends wg 'modules: array', 'activeKey?: int', 'settingLink?: string', - 'closeLink: string', 'menuLink: string', 'title?: string', 'linkParams?: string="%s"', @@ -34,7 +33,7 @@ class docMenu extends wg return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); } - private function buildLink($item): string + private function buildLink($item, $releaseID = 0): string { $url = $item->url; if(!empty($url)) return $url; @@ -88,6 +87,19 @@ class docMenu extends wg $linkParams = str_replace(array('browseType=&', 'param=0'), array('browseType=byrelease&', "param={$this->release}"), $linkParams); } } + + if($releaseID) + { + if($this->currentModule == 'doc') + { + $linkParams = str_replace(array('browseType=&', 'param=0'), array('browseType=byrelease&', "param={$releaseID}"), $linkParams); + if($this->rawMethod == 'view') $linkParams = "libID={$this->libID}&moduleID=0&browseType=byrelease&orderBy=&status,id_desc¶m={$releaseID}"; + } + else + { + $linkParams = "libID={$this->libID}&moduleID=0&apiID=0&version=0&release={$releaseID}"; + } + } return helper::createLink($moduleName, $methodName, $linkParams); } @@ -96,9 +108,12 @@ class docMenu extends wg if(empty($items)) $items = $this->modules; if(empty($items)) return array(); - $activeKey = $this->prop('activeKey'); + $activeKey = $this->prop('activeKey'); + $parentItems = array(); foreach($items as $setting) { + if(!is_object($setting)) continue; + $setting->parentID = $parentID; $itemID = 0; @@ -109,8 +124,9 @@ class docMenu extends wg 'text' => $setting->name, 'icon' => $this->getIcon($setting), 'url' => $this->buildLink($setting), + 'attrs' => array('data-app' => $this->tab), 'data-id' => $itemID, - 'data-lib' => $setting->type == 'docLib' ? $itemID : $setting->libID, + 'data-lib' => in_array($setting->type, array('docLib', 'apiLib')) ? $itemID : $setting->libID, 'data-type' => $setting->type, 'data-parent' => $setting->parentID, 'data-module' => $this->currentModule, @@ -134,6 +150,7 @@ class docMenu extends wg { global $app, $lang; $this->lang = $lang; + $this->tab = $app->tab; $this->rawModule = $app->rawModule; $this->rawMethod = $app->rawMethod; $this->currentModule = $app->moduleName; @@ -149,7 +166,7 @@ class docMenu extends wg $this->spaceMethod = $this->prop('spaceMethod'); if($this->rawModule == 'api' && $this->rawMethod == 'view') $this->spaceType = 'api'; - if($this->spaceType != 'project') + if(empty($this->modules['project'])) { $this->setProp('items', $this->buildMenuTree(array(), $this->libID)); } @@ -189,24 +206,56 @@ class docMenu extends wg 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', + $versionBtn = array(); + if(isset($item->versions) && $item->versions) + { + global $lang; + $versionTitle = $lang->build->common; + $versionBtn = array( + 'key' => 'version', + 'text' => $versionTitle, 'type' => 'dropdown', - 'caret' => false, 'dropdown' => array( 'placement' => 'bottom-end', - 'items' => $actions, + 'items' => array(), ) - ) - ); + ); + + foreach($item->versions as $version) + { + if($version->id == $this->release) $versionBtn['text'] = $version->version; + + $versionBtn['dropdown']['items'][] = array( + 'text' => $version->version, + 'href' => $this->buildLink($item, $version->id), + 'active' => $version->id == $this->release, + ); + } + } + + $moreBtn = array(); + if(!isset($item->hasAction) || $item->hasAction || in_array($item->type, array('mine', 'view', 'collect', 'createdBy', 'editedBy'))) + { + $actions = $this->getOperateItems($item); + if($actions) + { + $moreBtn = array( + 'key' => 'more', + 'icon' => 'ellipsis-v', + 'type' => 'dropdown', + 'caret' => false, + 'dropdown' => array( + 'placement' => 'bottom-end', + 'items' => $actions, + ) + ); + } + } + + $actions = array(); + if($versionBtn) $actions[] = $versionBtn; + if($moreBtn) $actions[] = $moreBtn; + return $actions ? $actions : null; } private function getOperateItems($item): array @@ -357,27 +406,6 @@ class docMenu extends wg ); } - 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(); @@ -385,32 +413,34 @@ class docMenu extends wg $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 + $menuLink ? dropmenu ( - setClass('module-title text-lg font-semibold'), - html($title) + set::id('docDropmenu'), + set::menuID('docDropmenuMenu'), + set::text($title), + set::url($menuLink), + ) : null, + 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) + ), + ) : 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->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(), ); } } diff --git a/lib/zin/wg/dropdown/v1.php b/lib/zin/wg/dropdown/v1.php index 8b9eb2f06a..ba65b0062d 100644 --- a/lib/zin/wg/dropdown/v1.php +++ b/lib/zin/wg/dropdown/v1.php @@ -116,7 +116,7 @@ class dropdown extends wg foreach($itemsList as $item) { if(!($item instanceof item)) continue; - $items[] = $item->props->toJsonData(); + $items[] = $item->props->toJSON(); } } foreach($items as $index => $item) diff --git a/lib/zin/wg/dropmenu/v1.php b/lib/zin/wg/dropmenu/v1.php index e5f8b71d35..5baeb8c415 100644 --- a/lib/zin/wg/dropmenu/v1.php +++ b/lib/zin/wg/dropmenu/v1.php @@ -38,7 +38,7 @@ class dropmenu extends wg 'text?: string', // 选择按钮上显示的文本。 'cache?: bool|int=true', // 是否启用缓存。 'data?: array', // 手动指定数据。 - 'menuID?: string="$GID"', // 指定下拉菜单的ID。 + 'menuID?: string', // 指定下拉菜单的ID。 ); /** @@ -58,13 +58,14 @@ class dropmenu extends wg * @access protected * @return wg */ - protected function build(): zui + protected function build(): array { 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'); + $app = data('app'); + $lang = data('lang'); + if(empty($menuID)) $menuID = $id . '-menu'; if(empty($tab)) $tab = $app->tab; if(empty($module)) $module = $app->rawModule; if(empty($method)) $method = $app->rawMethod; @@ -76,6 +77,46 @@ class dropmenu extends wg if(isset($object->id)) $objectID = $object->id; } + $branchMenu = null; + if(($tab == 'product' || $tab == 'qa') and in_array($module, $app->config->hasBranchMenuModules)) + { + if($objectID) + { + $product = $app->control->loadModel('product')->getByID((int)$objectID); + if($product->type != 'normal') + { + $branchID = data('branchID'); + + /* Get current branch name. */ + $branchName = ''; + if($branchID == 'all' || $branchID === '') + { + $branchID = 'all'; + $branchName = $lang->branch->all; + } + elseif($branchID == 0) + { + $branchName = $lang->branch->main; + } + elseif($branchID > 0) + { + $branchName = $app->control->loadModel('branch')->getById((int)$branchID); + } + + $branchURL = createLink('branch', 'ajaxGetDropMenu', "objectID=$objectID&branch=$branchID&module=$module&method=$method&extra=$extra"); + $branchMenu = zui::dropmenu + ( + setID('branch-dropmenu'), + set('_id', 'branch-dropmenu'), + set('_props', array('data-fetcher' => $branchURL)), + set('data', $data), + set(array('fetcher' => $branchURL, 'text' => $branchName, 'defaultValue' => $branchID)), + set($this->getRestProps()) + ); + } + } + } + if($tab == 'admin') { $currentMenuKey = $app->control->loadModel('admin')->getMenuKey(); @@ -91,7 +132,7 @@ class dropmenu extends wg $text = $object->name; } - return zui::dropmenu + return array(zui::dropmenu ( setID($menuID), set('_id', $id), @@ -99,6 +140,6 @@ class dropmenu extends wg set('data', $data), set(array('fetcher' => $url, 'text' => $text, 'defaultValue' => $objectID, 'cache' => $cache)), set($this->getRestProps()) - ); + ), $branchMenu); } } diff --git a/lib/zin/wg/dynamic/css/v1.css b/lib/zin/wg/dynamic/css/v1.css index 4b8c6417fc..d3c3d210fb 100644 --- a/lib/zin/wg/dynamic/css/v1.css +++ b/lib/zin/wg/dynamic/css/v1.css @@ -6,7 +6,7 @@ .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 + li:after {position: absolute; top: -12px; bottom: 20px; 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);} diff --git a/lib/zin/wg/echarts/v1.php b/lib/zin/wg/echarts/v1.php index a1fc35a255..ff890767c3 100644 --- a/lib/zin/wg/echarts/v1.php +++ b/lib/zin/wg/echarts/v1.php @@ -29,6 +29,6 @@ class echarts extends wg 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)))")); + return zui::echarts(inherit($this), set::_call("~((name,selector,options) => $.getLib('$jsFile', {root: false}, () => zui.create(name,selector,options)))")); } } diff --git a/lib/zin/wg/editor/css/v1.css b/lib/zin/wg/editor/css/v1.css index 911b4b5122..ec16ea734f 100644 --- a/lib/zin/wg/editor/css/v1.css +++ b/lib/zin/wg/editor/css/v1.css @@ -2,10 +2,11 @@ textarea[size="sm"], tiptap-editor[size="sm"] {min-height: 142px;} textarea[size="lg"], tiptap-editor[size="lg"] {min-height: 250px;} +textarea[size="full"] {min-height: 100%; height: 100%;} [data-tippy-root] tiptap-menu-item > button {font-size: inherit; font-family: Arial; line-height: normal;} -.editor-container {height: auto; width: 100%;} +.editor-container {width: 100%;} .editor-container tiptap-editor {display: none;} .editor-container textarea {height: initial;} [data-loaded-editor] .editor-container tiptap-editor {display: block;} diff --git a/lib/zin/wg/editor/v1.php b/lib/zin/wg/editor/v1.php index 51b5b664fc..e88d1aa4da 100644 --- a/lib/zin/wg/editor/v1.php +++ b/lib/zin/wg/editor/v1.php @@ -30,7 +30,7 @@ class editor extends wg // 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;});'; + return '$.getLib("' . $jsFile . '", {type: "module", root: false}, () => {document.body.dataset.loadedEditor = true;});'; } protected function build(): wg @@ -38,7 +38,8 @@ class editor extends wg $editor = new h ( setTag('tiptap-editor'), - setClass('form-control', 'p-0', 'h-auto'), + setClass('form-control', 'p-0'), + $this->prop('size') === 'full' ? setStyle('height', '100%') : setClass('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) @@ -56,7 +57,8 @@ class editor extends wg return div ( - setClass('editor-container'), + setClass('editor-container p-px'), + $props['size'] === 'full' ? setStyle('height', '100%') : setClass('h-auto'), $editor, textarea ( diff --git a/lib/zin/wg/filelist/v1.php b/lib/zin/wg/filelist/v1.php index 5babfe13ab..d0faa664ff 100644 --- a/lib/zin/wg/filelist/v1.php +++ b/lib/zin/wg/filelist/v1.php @@ -13,6 +13,7 @@ class fileList extends wg 'showDelete?:bool=true', 'showEdit?:bool=true', 'object?:object', + 'padding?:bool=true', ); public static function getPageCSS(): string|false @@ -77,7 +78,8 @@ class fileList extends wg return $fieldset ? new section ( - setClass('files', 'pt-4', $px, $pb, 'canvas'), + setClass('files', 'pt-4', 'canvas'), + $this->prop('padding') ? setClass($px, $pb) : null, set::title($lang->files), to::actions ( diff --git a/lib/zin/wg/floatprenextbtn/v1.php b/lib/zin/wg/floatprenextbtn/v1.php index 057e0400dc..8e0a8e74ad 100644 --- a/lib/zin/wg/floatprenextbtn/v1.php +++ b/lib/zin/wg/floatprenextbtn/v1.php @@ -18,12 +18,12 @@ class floatPreNextBtn extends wg { return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); } - protected function build(): wg + protected function build(): wg|array { $preLink = $this->prop('preLink'); $nextLink = $this->prop('nextLink'); - return fragment + return array ( !empty($preLink) ? btn ( diff --git a/lib/zin/wg/floattoolbar/v1.php b/lib/zin/wg/floattoolbar/v1.php index af9815db4c..0c467f6ae8 100644 --- a/lib/zin/wg/floattoolbar/v1.php +++ b/lib/zin/wg/floattoolbar/v1.php @@ -22,9 +22,9 @@ class floatToolbar extends wg return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); } - private function buildDivider(wg|array|null $wg): wg|null + private function buildDivider(wg|array|null $wg1, wg|array|null $wg2): wg|null { - if(empty($wg)) return null; + if(empty($wg1) || empty($wg2)) return null; return div(setClass('divider w-px h-6 mx-2')); } @@ -92,9 +92,9 @@ class floatToolbar extends wg ( setClass('float-toolbar inline-flex rounded p-1.5 items-center'), $prefixBtns, - $this->buildDivider($prefixBtns), + $this->buildDivider($prefixBtns, $mainBtns), $mainBtns, - empty($mainBtns) ? null : $this->buildDivider($suffixBtns), + $this->buildDivider($mainBtns, $suffixBtns), $suffixBtns, ); } diff --git a/lib/zin/wg/formbatchitem/v1.php b/lib/zin/wg/formbatchitem/v1.php index 8757a6c4f3..ba885d053e 100644 --- a/lib/zin/wg/formbatchitem/v1.php +++ b/lib/zin/wg/formbatchitem/v1.php @@ -30,7 +30,7 @@ class formBatchItem extends wg */ protected static array $defineProps = array( 'name: string', // 表单项名称,无需包含 `[]`。 - 'label?: string|bool', // 列标题。 + 'label: string|bool', // 列标题。 'labelClass?: string', // 列标题类名。 'labelProps?: string', // 列标题属性,例如 `array('data-toggle' => 'tooltip', 'data-title' 。=> 'This is a tip')` 'required?:bool|string="auto"', // 是否必填,如果设置为 `"auto"`,则自动从当前模块 config 中查询。 @@ -80,6 +80,7 @@ class formBatchItem extends wg $asIndex = $control['type'] === 'index'; if($asIndex) $control['type'] = 'static'; + if($control['type'] == 'static') $name .= '_static'; return array( h::th diff --git a/lib/zin/wg/formlabel/v1.php b/lib/zin/wg/formlabel/v1.php index 7f4a112ace..2c391115c6 100644 --- a/lib/zin/wg/formlabel/v1.php +++ b/lib/zin/wg/formlabel/v1.php @@ -27,7 +27,7 @@ class formLabel extends wg setClass('form-label', $required ? 'required' : null), set('for', $for), set($this->getRestProps()), - $text, + html($text), $this->children(), ); } diff --git a/lib/zin/wg/formpanel/js/v1.js b/lib/zin/wg/formpanel/js/v1.js index c173efd167..9f254e76dd 100644 --- a/lib/zin/wg/formpanel/js/v1.js +++ b/lib/zin/wg/formpanel/js/v1.js @@ -40,20 +40,18 @@ function hideSingleField(hiddenFields) function hideBatchField(hiddenFields) { if(typeof hiddenFields == 'undefined') return false; + if(hiddenFields.length == 0) return false; - hiddenFields.forEach(function(field) - { - $('th.form-batch-head[data-name="' + field + '"]').addClass('hidden'); - $('td.form-batch-control[data-name="' + field + '"]').addClass('hidden'); - $($('template.form-batch-template')[0].content).find('td.form-batch-control[data-name="' + field + '"]').addClass('hidden'); - }); + if(hiddenFields.includes('source')) hiddenFields.push('sourceNote'); + batchForm = $('th.form-batch-head').closest('.form-batch').zui('batchForm'); + batchForm.toggleCols(hiddenFields, false); } if(typeof hiddenFields != 'undefined') { if(typeof formBatch == 'undefined' || !formBatch) { - hideSingleField(hiddenFields); + setTimeout(function(){hideSingleField(hiddenFields)}, 300); } else { diff --git a/lib/zin/wg/formpanel/v1.php b/lib/zin/wg/formpanel/v1.php index c82c615848..d1c62a7904 100644 --- a/lib/zin/wg/formpanel/v1.php +++ b/lib/zin/wg/formpanel/v1.php @@ -67,6 +67,40 @@ class formPanel extends panel return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); } + protected function created() + { + $customFields = $this->prop('customFields'); + if($customFields === true) + { + global $app, $config, $lang; + $module = $app->rawModule; + $method = $app->rawMethod; + + $app->loadLang($module); + $app->loadModuleConfig($module); + + $key = $method . 'Fields'; + $listFields = array(); + $listFieldsKey = 'custom' . ucfirst($key); + if(!empty($config->$module->$listFieldsKey)) $listFields = explode(',', $config->$module->$listFieldsKey); + if(!empty($config->$module->list->$listFieldsKey)) $listFields = explode(',', $config->$module->list->$listFieldsKey); + + if(empty($listFields)) + { + $this->setProp('customFields', array()); + return false; + } + + $fields = array(); + foreach($listFields as $field) $fields[$field] = $lang->$module->$field; + + $showFields = explode(',', $config->$module->custom->$key); + $customFields = array('list' => $fields, 'show' => $showFields, 'key' => $key); + + $this->setProp('customFields', $customFields); + } + } + /** * Build heading actions. * @@ -78,19 +112,28 @@ class formPanel extends panel $headingActions = $this->prop('headingActions'); if(!$headingActions) $headingActions = array(); - $customFields = $this->prop('customFields'); - /* Custom fields. */ + $customFields = $this->prop('customFields', array()); if($customFields) { global $app; - $urlParams = isset($customFields['urlParams']) ? $customFields['urlParams'] : "module={$app->rawModule}§ion=custom&key=batchCreateFields"; + $listFields = zget($customFields, 'list', array()); + $showFields = zget($customFields, 'show', array()); + $key = zget($customFields, 'key', $app->rawMethod); - $headingActions[] = formSettingBtn(set::customFields($customFields['items']), set::urlParams($urlParams)); + if($listFields && $key) + { + $urlParams = "module={$app->rawModule}§ion=custom&key={$key}"; + $headingActions[] = formSettingBtn + ( + set::customFields(array('list' => $listFields, 'show' => $showFields)), + set::urlParams(zget($customFields, 'urlParams', $urlParams)), + ); + + $this->setProp('headingActions', $headingActions); + } } - $this->setProp('headingActions', $headingActions); - return parent::buildHeadingActions(); } @@ -102,15 +145,10 @@ class formPanel extends panel */ protected function buildForm(): wg { - $customFields = $this->prop('customFields'); - $hiddenFields = array(); - if(!empty($customFields['items'])) - { - $hiddenFields = array_values(array_filter(array_map(function($item) - { - return $item['show'] ? false : $item['name']; - }, $customFields['items']))); - } + $customFields = $this->prop('customFields', array()); + $listFields = zget($customFields, 'list', array()); + $showFields = zget($customFields, 'show', array()); + $hiddenFields = $listFields && $showFields ? array_values(array_diff(array_keys($listFields), $showFields)) : array(); if($this->prop('batch')) { @@ -160,7 +198,8 @@ class formPanel extends panel { return div ( - setClass('panel-body'), + setClass('panel-body ' . $this->prop('bodyClass')), + set($this->prop('bodyProps')), $this->buildForm() ); } diff --git a/lib/zin/wg/formrowgroup/v1.php b/lib/zin/wg/formrowgroup/v1.php index ac312e415a..412d54a500 100644 --- a/lib/zin/wg/formrowgroup/v1.php +++ b/lib/zin/wg/formrowgroup/v1.php @@ -34,6 +34,7 @@ class formRowGroup extends formRow $this->prop('title'), $this->block('suffix'), ), + set($this->getRestProps()), $this->prop('items'), ); } diff --git a/lib/zin/wg/formsettingbtn/js/v1.js b/lib/zin/wg/formsettingbtn/js/v1.js index 46dd5ea891..99d25ffa8b 100644 --- a/lib/zin/wg/formsettingbtn/js/v1.js +++ b/lib/zin/wg/formsettingbtn/js/v1.js @@ -1,27 +1,35 @@ function closeCustomPopupMenu(e) { - $(e.target).closest('menu').removeClass('show'); + $('#formSettingBtn-toggle').trigger('click'); +} + +function cancelFormSetting(e) +{ + $.get($(e.target).closest('button').data('url'), function(data) + { + e.target.closest('form').querySelectorAll('input').forEach(function(field) + { + const checked = `,${data},`.indexOf(`,${$(field).val()},`) >=0; + $(field).prop('checked', checked); + }); + }); + + closeCustomPopupMenu(e); } function revertDefaultFields(e) { - $.get($(e.target).closest('button').data('url'), function(data, status) + $.getJSON($(e.target).closest('button').data('url'), function(response, status) { - const customFields = []; - const showFields = []; - e.target.closest('form').querySelectorAll('input').forEach(function(field) + const customFields = response.customFields.split(','); + const showFields = response.showFields.split(','); + showFields.forEach(function(field) { - /* Gather all custom fields. */ - customFields.push(field.value); - if(field.value === 'source') customFields.push('sourceNote'); + if(!field) return true; - $(field).prop('checked', false); - if(!$(field).data('default')) return; - - /* Gather checked fields to be visible. */ - $(field).prop('checked', true); - showFields.push(field.value); - if(field.value === 'source') showFields.push('sourceNote'); + var $fieldCheckbox = $(e.target).closest('form').find('input[type=checkbox][value="' + field + '"]'); + if($fieldCheckbox.length == 0) return false; + $fieldCheckbox.prop('checked', true); }); hideAndShowFormFields(customFields, showFields); @@ -68,38 +76,47 @@ function toggleSingleField(customFields, showFields) customFields.forEach(function(field) { - var $this = $('form [name^="' + field + '"]'); - if($this.length == 0) return; + let $field = $('form [name^="' + field + '"]'); + if($field.length == 0) return; - var hidden = !showFields.includes(field); - var $inputGroup = $this.closest('.input-group'); - var $formGroup = $this.closest('.form-group'); + let hidden = !showFields.includes(field); + let $formRow = $field.closest('.form-row'); + let $formGroup = $field.closest('.form-group'); + let $inputGroup = $field.closest('.input-group'); + let $inputControl = $field.closest('.input-control'); if($inputGroup.length == 1) { - $prev = $this.prev(); + $prev = $field.prev(); if($prev.hasClass('input-group-addon')) $prev.toggleClass('hidden', hidden); - $this.toggleClass('hidden', hidden); - if($this.hasClass('pick-value')) + $field.toggleClass('hidden', hidden); + if($field.hasClass('pick-value')) { - $pickBox = $this.closest('.pick').parent(); + $pickBox = $field.closest('.pick').parent(); $pickBox.toggleClass('hidden', hidden); $prev = $pickBox.prev(); if($prev.hasClass('input-group-addon')) $prev.toggleClass('hidden', hidden); } + + if($inputControl.length == 1) + { + if($inputControl.hasClass('has-suffix')) + { + if($inputControl.prev().hasClass('input-group-addon')) $inputControl.prev().toggleClass('hidden', hidden); + $inputControl.toggleClass('hidden', hidden); + } + } + + if($inputGroup.prev().hasClass('form-label')) $inputGroup.prev().toggleClass('hidden', hidden); + $inputGroup.toggleClass('hidden', $inputGroup.children().length == $inputGroup.children('.hidden').length); + $formGroup.toggleClass('hidden', $formGroup.children().length == $formGroup.children('.hidden').length); } else { $formGroup.toggleClass('hidden', hidden); } - }) - - $('form .form-row').each(function() - { - var $this = $(this); - $this.removeClass('hidden'); - if($this.find('.form-group.hidden').length > 0 && $this.find('.form-group:not(.hidden)').length == 0) $this.addClass('hidden'); + $formRow.toggleClass('hidden', $formRow.children().length == $formRow.children('.hidden').length); }); } @@ -107,11 +124,28 @@ function toggleBatchField(customFields, showFields) { if(typeof customFields == 'undefined') return false; + let hiddenFields = []; + let shownFields = []; customFields.forEach(function(field) { - var hidden = !showFields.includes(field); + if($('th.form-batch-head[data-name="' + field + '"]').length == 0) return true; + + let hidden = !showFields.includes(field); $('th.form-batch-head[data-name="' + field + '"]').toggleClass('hidden', hidden); $('td.form-batch-control[data-name="' + field + '"]').toggleClass('hidden', hidden); $($('template.form-batch-template')[0].content).find('td.form-batch-control[data-name="' + field + '"]').toggleClass('hidden', hidden); + if(field === 'source') + { + $('th.form-batch-head[data-name="sourceNote"]').toggleClass('hidden', hidden); + $('td.form-batch-control[data-name="sourceNote"]').toggleClass('hidden', hidden); + $($('template.form-batch-template')[0].content).find('td.form-batch-control[data-name="sourceNote"]').toggleClass('hidden', hidden); + } + + hidden ? hiddenFields.push(field) : shownFields.push(field); + if(field === 'source') hidden ? hiddenFields.push('sourceNote') : shownFields.push('sourceNote'); }); + + batchForm = $('th.form-batch-head').closest('.form-batch').zui('batchForm'); + if(hiddenFields.length > 0) batchForm.toggleCols(hiddenFields, false); + if(shownFields.length > 0) batchForm.toggleCols(shownFields, true); } diff --git a/lib/zin/wg/formsettingbtn/v1.php b/lib/zin/wg/formsettingbtn/v1.php index baf74b892b..dc1a3d03c6 100644 --- a/lib/zin/wg/formsettingbtn/v1.php +++ b/lib/zin/wg/formsettingbtn/v1.php @@ -29,6 +29,26 @@ class formSettingBtn extends wg return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); } + private function buildCustomFields(array $customFields): array + { + $listFields = zget($customFields, 'list', array()); + $showFields = zget($customFields, 'show', array()); + if(!$listFields) return array(); + + $items = array(); + foreach($listFields as $field => $text) + { + $items[] = checkbox + ( + set::name('fields[]'), + set::value($field), + set::text($text), + set::checked($showFields ? in_array($field, $showFields) : true) + ); + } + return $items; + } + protected function build(): wg { $customFields = $this->prop('customFields', array()); @@ -36,10 +56,12 @@ class formSettingBtn extends wg global $lang; $customLink = createLink('custom', 'ajaxSaveCustomFields', $this->prop('urlParams', '')); + $cancelLink = createLink('custom', 'ajaxGetCustomFields', $this->prop('urlParams', '')); return dropdown ( set::arrow('false'), set::placement('bottom-end'), + set::id('formSettingBtn'), to::trigger(btn(set::icon('cog-outline'), setClass('ghost'), set::caret(false))), to::menu(menu ( @@ -53,21 +75,11 @@ class formSettingBtn extends wg set::actions(array ( btn(set::text($lang->save), setClass('primary'), on::click('onSubmitFormtSetting')), - btn(set::text($lang->cancel), set::btnType('reset'), on::click('closeCustomPopupMenu')), - btn(set::text($lang->restore), setClass('text-primary ghost font-bold'), set::href('#'), set('data-url', $customLink), on::click('revertDefaultFields')), + btn(set::text($lang->cancel), set::btnType('button'), on::click('cancelFormSetting'), set('data-url', $cancelLink)), + btn(set::text($lang->restore), setClass('text-primary ghost'), set::href('#'), set('data-url', $customLink), on::click('revertDefaultFields')), )), to::headingActions(array(btn(set::icon('close'), setClass('ghost'), set::size('sm'), on::click('closeCustomPopupMenu')))), - array_map(function($field) - { - return checkbox - ( - set::name('fields[]'), - set::value($field['name']), - set::text($field['text']), - set::checked(isset($field['show']) ? $field['show'] : false), - set('data-default', isset($field['default']) ? $field['default'] : false) - ); - }, $customFields), + $this->buildCustomFields($customFields) ) )) ); diff --git a/lib/zin/wg/gantt/v1.php b/lib/zin/wg/gantt/v1.php new file mode 100644 index 0000000000..7a980cfc82 --- /dev/null +++ b/lib/zin/wg/gantt/v1.php @@ -0,0 +1,20 @@ +getWebRoot() . 'js/dhtmlxgantt/min.js'; + + return zui::gantt(inherit($this), set::_call("~((name,selector,options) => $.getLib('$jsFile', {root: false}, () => {gantt.plugins({marker: true, critical_path: true, fullscreen: true, tooltip: true, click_drag: true});zui.create(name,selector,options);}))")); + + } +} diff --git a/lib/zin/wg/globalsearch/js/v1.js b/lib/zin/wg/globalsearch/js/v1.js new file mode 100644 index 0000000000..7efa6855e3 --- /dev/null +++ b/lib/zin/wg/globalsearch/js/v1.js @@ -0,0 +1,74 @@ +$(function() +{ + const $searchQuery = $('#globalSearchInput'); + const setSelected = function() + { + $searchQuery.data('selectedKey', getSearchType()); + }; + + $searchQuery.on('change keyup paste input propertychange', setSelected).on('focus', function() + { + setTimeout(setSelected, 300); + }); +}); + +/** + * Get the search Type according to the current module and the current method. + * + * @access public + * @return string + */ +function getSearchType() +{ + if(vision == 'lite') return 'story'; + + const appInfo = $.apps.getLastApp(); + const appPageModuleName = appInfo.iframe.contentWindow.config.currentModule; + const appPageMethodName = appInfo.iframe.contentWindow.config.currentMethod; + + if(appPageModuleName == 'product' && appPageMethodName == 'browse') return 'story'; + if(appPageModuleName == 'my' || appPageModuleName == 'user') return appPageMethodName; + + const projectMethod = 'task|story|bug|build'; + if(appPageModuleName == 'project' && projectMethod.indexOf(appPageMethodName) != -1) return appPageMethodName; + + if(searchObjectList.indexOf(appPageModuleName) == -1) return 'bug'; + + return appPageModuleName; +} + +function getSearchUrl(searchType, searchValue) +{ + if(searchValue) + { + const reg = /[^0-9]/; + let searchUrl = $.createLink('search', 'index'); + searchUrl += (searchUrl.indexOf('?') >= 0 ? '&' : '?') + 'words=' + searchValue + '&type=all'; + if(!searchType || searchType == 'all' || reg.test(searchValue)) return searchUrl; + + const types = searchType.split('-'); + const searchModule = types[0]; + const searchMethod = typeof(types[1]) == 'undefined' ? 'view' : types[1]; + searchUrl = $.createLink(searchModule, searchMethod, "id=" + searchValue); + const assetType = ',story,issue,risk,opportunity,doc,'; + if(assetType.indexOf(',' + searchModule + ',') == -1) return searchUrl; + + const link = $.createLink('index', 'ajaxGetViewMethod' , 'objectID=' + searchValue + '&objectType=' + searchModule); + $.get(link, function(data) + { + if(data) return $.createLink('assetlib', data, "id=" + searchValue); + }); + } + + return false; +} + +window.globalSearch = function(key, searchValue) +{ + let searchType = key; + if(key == 'program') searchType = 'program-product'; + if(key == 'deploystep') searchType = 'deploy-viewstep'; + + const searchUrl = getSearchUrl(searchType, searchValue); + if(searchUrl) openUrl(searchUrl); +}; diff --git a/lib/zin/wg/globalsearch/v1.php b/lib/zin/wg/globalsearch/v1.php index 6367ac67da..1c325d91fc 100644 --- a/lib/zin/wg/globalsearch/v1.php +++ b/lib/zin/wg/globalsearch/v1.php @@ -8,12 +8,35 @@ class globalSearch extends wg 'commonSearchText?: string', 'commonSearchUrl: string', 'searchItems: array', + 'searchFunc: callable' ); + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + protected function build(): array { - global $lang; - $this->setDefaultProps(array('commonSearchText' => $lang->searchAB)); + global $config, $lang; + + jsVar('searchObjectList', array_keys($lang->searchObjects)); + + if($config->systemMode == 'light') unset($lang->searchObjects['program']); + unset($lang->searchObjects['all']); + + $searchItems = array(); + foreach($lang->searchObjects as $key => $module) + { + $searchItems[] = array('key' => $key, 'text' => $module); + } + + $this->setDefaultProps(array( + 'commonSearchText' => $lang->searchAB, + 'commonSearchKey' => 'all', + 'searchItems' => $searchItems, + 'searchFunc' => jsRaw('window.globalSearch'), + )); $input = inputGroup ( @@ -32,7 +55,7 @@ class globalSearch extends wg $input->setProp('data-zin-id', $input->gid); $props = array_merge ( - $this->props->pick(array('commonSearchText', 'commonSearchUrl', 'searchItems')), + $this->props->pick(array('commonSearchText', 'commonSearchKey', 'searchItems', 'searchFunc')), array('_to' => "[data-zin-id='{$input->gid}']") ); return array( diff --git a/lib/zin/wg/header/js/v1.js b/lib/zin/wg/header/js/v1.js new file mode 100644 index 0000000000..1787b15e2b --- /dev/null +++ b/lib/zin/wg/header/js/v1.js @@ -0,0 +1,6 @@ +window.updateUserAvatar = function(dialog) +{ + const newSrc = $(dialog).find('.avatar-img').prop('src'); + const code = $('#toolbar').find('.avatar-img').dataset('code'); + $('.avatar-img[data-code="' + code + '"]').prop('src', newSrc); +}; diff --git a/lib/zin/wg/header/v1.php b/lib/zin/wg/header/v1.php index fb4439f537..63bd702a61 100644 --- a/lib/zin/wg/header/v1.php +++ b/lib/zin/wg/header/v1.php @@ -18,6 +18,11 @@ class header extends wg 'toolbar' => array('map' => 'btn') ); + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + protected function buildHeading() { if($this->hasBlock('heading')) return $this->block('heading'); @@ -87,14 +92,7 @@ class header extends wg /* The standalone lite version removes the lite interface button */ if(trim($config->visions, ',') == 'lite') return true; - if(count($userVisions) < 2 || count($configVisions) < 2) - { - return btn - ( - setClass('secondary ring-0 rounded'), - $lang->visionList[$currentVision] - ); - } + if(count($userVisions) < 2 || count($configVisions) < 2) return btn($lang->visionList[$currentVision]); $items = array(); foreach($userVisions as $vision) @@ -116,6 +114,7 @@ class header extends wg set::text($lang->visionList[$currentVision]), set::caret(false) ), + set::id('versionMenu'), set::trigger('hover'), set::placement('bottom'), @@ -145,7 +144,7 @@ class header extends wg 'href' => createLink('my', 'profile', '', '', true), 'className' => 'items-center gap-2 px-2 py-1 row text-inherit', 'style' => array('padding-left' => 0), - 'data-toggle' => 'iframeModal', + 'data-toggle' => 'modal', 'data-size' => 700, 'data-id' => 'profile', 'renders' => array(array('__html' => implode('', array @@ -168,7 +167,7 @@ class header extends wg 'icon' => 'account', 'text' => $lang->profile, 'class' => 'iframe', - 'data-toggle' => 'iframeModal', + 'data-toggle' => 'modal', 'data-size' => 700, 'data-id' => 'profile', ); @@ -199,7 +198,7 @@ class header extends wg 'text' => $lang->preference, 'class' => 'iframe', 'data-width' => 700, - 'data-toggle' => 'iframeModal' + 'data-toggle' => 'modal' ); } @@ -210,7 +209,7 @@ class header extends wg 'url' => createLink('my', 'changepassword', '', '', true), 'icon' => 'cog-outline', 'text' => $lang->changePassword, - 'data-toggle' => 'iframeModal', + 'data-toggle' => 'modal', 'data-size' => 'sm' ); } @@ -241,12 +240,12 @@ class header extends wg $helpItems = array(); $manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme']; $helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help')); - $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-width' => 800, 'data-toggle' => 'iframeModal', 'data-headerless' => true, 'data-keyboard' => true, 'data-backdrop' => true); + $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal'); $items[] = array('text' => $lang->help, 'icon' => 'help', 'items' => $helpItems); /* printClientLink */ - $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'iframeModal', 'data-width' => 1050, 'data-headerless' => true, 'data-keyboard' => true, 'data-backdrop' => true); + $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal'); $items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX); $items[] = array('type' => 'divider'); @@ -257,7 +256,7 @@ class header extends wg } else { - $items[] = array('text' => $lang->logout, 'url' => createLink('user', 'logout'), 'target' => '_top', 'icon' => 'exit'); + $items[] = array('text' => $lang->logout, 'url' => "javascript:$.apps.logout()", 'icon' => 'exit'); } return dropdown @@ -347,7 +346,7 @@ class header extends wg case 'doc': $params = "objectType=&objectID=0&libID=0"; $createMethod = 'selectLibType'; - $item['data-toggle'] = 'iframeModal'; + $item['data-toggle'] = 'modal'; break; case 'project': if($config->vision == 'lite') @@ -358,7 +357,7 @@ class header extends wg { $params = "programID=0&from=global"; $createMethod = 'createGuide'; - $item['data-toggle'] = 'iframeModal'; + $item['data-toggle'] = 'modal'; } else { diff --git a/lib/zin/wg/history/css/v1.css b/lib/zin/wg/history/css/v1.css index a5bd86d062..4ea1f368a0 100644 --- a/lib/zin/wg/history/css/v1.css +++ b/lib/zin/wg/history/css/v1.css @@ -10,7 +10,7 @@ .history-list > li strong {color: var(--color-slate-800);} .history-list > li {position: relative;} .history-list > li::before {display: block; content: ''; position: absolute; width: 1px; height: 100%; background: var(--color-gray-200); top: 12px; left: 10px;} -.history-list > li:last-child::before {display: none;} .history-list.sort-reverse > li::before {display: block; content: ''; position: absolute; width: 1px; height: 100%; background: var(--color-gray-200); top: 12px; left: 10px;} -.history-list.sort-reverse > li:first-child::before {display: none;} .history-list.sort-reverse > li:last-child::before {display: block;} +.history-list > li:last-child::before {display: none;} +.history-list.sort-reverse > li:first-child::before {display: none;} diff --git a/lib/zin/wg/history/v1.php b/lib/zin/wg/history/v1.php index 3f9977d4df..ade13d2d47 100644 --- a/lib/zin/wg/history/v1.php +++ b/lib/zin/wg/history/v1.php @@ -13,6 +13,7 @@ class history extends wg 'commentUrl?: string', 'commentBtn?: bool', 'bodyClass?: string', + 'hasComment?: bool', ); public static function getPageCSS(): string|false @@ -25,12 +26,12 @@ class history extends wg return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); } - private function marker(int $num): wg + private function marker(int|string $content): wg { return span ( setClass('marker', 'relative', 'z-10', 'text-sm', 'rounded-full', 'aspect-square', 'inline-flex', 'justify-center', 'items-center', 'mr-1', 'border', 'h-5', 'w-5', 'z-10'), - $num + is_int($content) ? $content : icon('check', setClass('text-success font-semibold')) ); } @@ -105,8 +106,7 @@ class history extends wg return li ( setClass('mb-2 flex'), - set::value($i), - $this->marker($i), + $this->marker($action->action === 'finished' ? 'finished' : $i), $actionItemView ); } @@ -231,7 +231,7 @@ class history extends wg $isInModal = isAjaxRequest('modal'); $padding = $isInModal ? 'px-3 pd-3' : 'px-6 pb-6'; - list($commentUrl, $bodyClass) = $this->prop(array('commentUrl', 'bodyClass')); + list($commentUrl, $bodyClass, $hasComment) = $this->prop(array('commentUrl', 'bodyClass', 'hasComment')); return panel ( setClass('history', 'pt-4', 'h-full', $padding), @@ -258,11 +258,11 @@ class history extends wg ) ), div(setClass('mt-3'), $this->historyList()), - commentDialog + $hasComment !== false ? commentDialog ( set::name('comment'), set::url($commentUrl), - ) + ) : null ); } } diff --git a/lib/zin/wg/imgcutter/v1.php b/lib/zin/wg/imgcutter/v1.php new file mode 100644 index 0000000000..a4e451be26 --- /dev/null +++ b/lib/zin/wg/imgcutter/v1.php @@ -0,0 +1,57 @@ +prop('btnText'); + $tipText = $this->prop('tipText'); + + $imgCutter = div + ( + set($this->getRestProps()), + setClass('img-cutter'), + div + ( + setClass('canvas'), + img(set::src($this->prop('src'))) + ), + div + ( + setClass('text-xl font-bold py-3'), + $tipText + ), + btn + ( + set::type('primary'), + $btnText + ) + ); + $imgCutter->setProp('data-zin-id', $imgCutter->gid); + + $props = array_merge($this->props->pick(array('coverColor', 'coverOpacity', 'defaultWidth', 'defaultHeight', 'minWidth', 'minHeight', 'fixedRatio', 'onSizeError', 'ready', 'handleBtnClick')), array('_to' => "[data-zin-id='{$imgCutter->gid}']")); + + return array( + $imgCutter, + zui::imgCutter(set($props)), + ); + } +} diff --git a/lib/zin/wg/input/v1.php b/lib/zin/wg/input/v1.php index 02ac3bdd77..d6ba61eced 100644 --- a/lib/zin/wg/input/v1.php +++ b/lib/zin/wg/input/v1.php @@ -6,7 +6,7 @@ class input extends wg { protected static array $defineProps = array( 'type: string', - 'name: string', + 'name?: string', 'id?: string', 'class?: string', 'value?: string', diff --git a/lib/zin/wg/inputcontrol/v1.php b/lib/zin/wg/inputcontrol/v1.php index f5d12719da..e5427c29dc 100644 --- a/lib/zin/wg/inputcontrol/v1.php +++ b/lib/zin/wg/inputcontrol/v1.php @@ -11,6 +11,7 @@ class inputControl extends wg 'suffix?: mixed', 'prefixWidth?: string|int', 'suffixWidth?: string|int', + 'class?: string', ); protected static array $defineBlocks = array( @@ -20,27 +21,27 @@ class inputControl extends wg protected function build(): wg { - list($prefix, $suffix, $prefixWidth, $suffixWidth) = $this->prop(['prefix', 'suffix', 'prefixWidth', 'suffixWidth']); + list($prefix, $suffix, $prefixWidth, $suffixWidth, $class) = $this->prop(['prefix', 'suffix', 'prefixWidth', 'suffixWidth', 'class']); if(empty($prefix)) $prefix = $this->block('prefix'); if(empty($suffix)) $suffix = $this->block('suffix'); - $class = array('input-control'); + $class = "input-control {$class}"; $vars = array(); if(!empty($prefix)) { if(is_numeric($prefixWidth)) { $vars['input-control-prefix'] = $prefixWidth . 'px'; - $class[] = 'has-prefix'; + $class .= ' has-prefix'; } elseif(!empty($prefixWidth)) { - $class[] = "has-prefix-$prefixWidth"; + $class .= " has-prefix-$prefixWidth"; } else { - $class[] = 'has-prefix'; + $class .= ' has-prefix'; } } if(!empty($suffix)) @@ -48,15 +49,15 @@ class inputControl extends wg if(is_numeric($suffixWidth)) { $vars['input-control-suffix'] = $suffixWidth . 'px'; - $class[] = 'has-suffix'; + $class .= ' has-suffix'; } elseif(!empty($suffixWidth)) { - $class[] = "has-suffix-$suffixWidth"; + $class .= " has-suffix-$suffixWidth"; } else { - $class[] = 'has-suffix'; + $class .= ' has-suffix'; } } diff --git a/lib/zin/wg/label/css/v1.css b/lib/zin/wg/label/css/v1.css new file mode 100644 index 0000000000..b9051f40e6 --- /dev/null +++ b/lib/zin/wg/label/css/v1.css @@ -0,0 +1,9 @@ +.label.status-clarify, .label.status-draft {--tw-ring-color: #8166ee;} +.label.status-cancel, .label.status-canceled, .label.status-investigate {--tw-ring-color: #838a9d;} +.label.status-canceled, .label.status-closed, .label.status-testtask .label.status-done {--tw-ring-color: #9ea3b0;} +.label.status-blocked, .label.status-hangup, .label.status-pause, .label.status-suspended {--tw-ring-color: #b89664;} +.label.status-noreview, .label.status-reviewing, .label.status-wait .label.status-testcase {--tw-ring-color: #18a6fd;} +.label.status-active .label.status-risk, .label.status-changed, .label.status-changing, .label.status-fail {--tw-ring-color: #fb2b2b;} +.label.status-active, .label.status-asked, .label.status-normal, .label.status-unconfirmed, .label.status-wait {--tw-ring-color: #313c52;} +.label.status-active .label.status-issue, .label.status-checking, .label.status-commenting, .label.status-confirmed, .label.status-doing {--tw-ring-color: #ff6f42;} +.label.status-checked, .label.status-done, .label.status-pass, .label.status-replied, .label.status-resolved, .label.status-success {--tw-ring-color: #0dbb7d;} diff --git a/lib/zin/wg/label/v1.php b/lib/zin/wg/label/v1.php index 8e1ea8c562..74c69e15aa 100644 --- a/lib/zin/wg/label/v1.php +++ b/lib/zin/wg/label/v1.php @@ -8,6 +8,11 @@ class label extends wg 'text?:string' ); + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + public function onAddChild($child) { if(is_string($child) && !$this->props->has('text')) diff --git a/lib/zin/wg/mainnavbar/css/v1.css b/lib/zin/wg/mainnavbar/css/v1.css new file mode 100644 index 0000000000..9a8dd2ba82 --- /dev/null +++ b/lib/zin/wg/mainnavbar/css/v1.css @@ -0,0 +1,12 @@ +#mainNavbar .main-navbar-left {position: absolute; z-index: 2;} +#mainNavbar .main-navbar-left #switcher {position: relative; top: 5px; left: 1rem;} +#mainNavbar .main-navbar-left #switcher > .dropmenu-btn {background: #FFF;} +#mainNavbar .main-navbar-left #switcher > .dropmenu-btn:before {background: unset;} +#mainNavbar .main-navbar-left #switcher:after, #mainNavbar .main-navbar-left #switcher:before {position: absolute; display: block; width: 0; height: 0; content: ' '; border-style: solid; border-width: 17px 0 17px 8px; top: -1px;} +#mainNavbar .main-navbar-left #switcher:before {border-color: transparent transparent transparent rgb(var(--color-primary-500-rgb)); right: -8px;} +#mainNavbar .main-navbar-left #switcher:after {border-color: transparent transparent transparent #FFF; right: -7px; border-radius: 2px;} +#mainNavbar .main-navbar-left #switcher .icon-angle-right {display: none;} +#mainNavbar .main-navbar-left #switcher .caret {color: rgb(var(--color-link-hover-rgb));} +#mainNavbar .main-navbar-left #switcher .text {color: rgb(var(--color-primary-500-rgb));} + +@media (min-width: 1400px) {#mainNavbar .main-navbar-left #switcher {left: 2.5rem;};} diff --git a/lib/zin/wg/mainnavbar/v1.php b/lib/zin/wg/mainnavbar/v1.php index 55e866db84..5c4e496a9e 100644 --- a/lib/zin/wg/mainnavbar/v1.php +++ b/lib/zin/wg/mainnavbar/v1.php @@ -34,6 +34,17 @@ class mainNavbar extends nav 'right' => array('map' => 'toolbar'), ); + /** + * 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'); + } + protected function created() { global $app; @@ -91,6 +102,7 @@ class mainNavbar extends nav $leftBlock = $this->block('left'); $rightBlock = $this->block('right'); + if(empty($leftBlock)) $leftBlock = $this->buildSwitcher(); return div ( @@ -104,4 +116,37 @@ class mainNavbar extends nav ) ); } + + /** + * 构建2.5级下拉菜单。 + * Build switcher. + * + * @access protected + * @return array + */ + protected function buildSwitcher(): array|null + { + global $app, $config; + + $moduleName = $app->rawModule; + $methodName = $app->rawMethod; + + if(in_array("$moduleName-$methodName", is_array($config->excludeSwitcherList) ? $config->excludeSwitcherList : array())) return null; + + if(in_array($moduleName, is_array($config->hasSwitcherModules) ? $config->hasSwitcherModules : array())) + { + $fetcher = createLink($moduleName, 'ajaxGetDropMenu', data('switcherParams')); + return array(zui::dropmenu + ( + setID("{$moduleName}-menu"), + set('_id', 'switcher'), + set('data', data('data')), + set('_props', array('data-fetcher' => $fetcher)), + set(array('fetcher' => createLink($moduleName, 'ajaxGetDropMenu', data('switcherParams')), 'text' => data('switcherText'), 'defaultValue' => data('switcherObjectID'))), + set($this->getRestProps()) + )); + } + + return null; + } } diff --git a/lib/zin/wg/mindmap/v1.php b/lib/zin/wg/mindmap/v1.php new file mode 100644 index 0000000000..df1262a3a8 --- /dev/null +++ b/lib/zin/wg/mindmap/v1.php @@ -0,0 +1,75 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +/** + * 脑图(mindmap)部件。 + * The mindmap widget class. + */ +class mindmap extends wg +{ + protected static array $defineProps = array + ( + 'data?: array', + 'width?: string|number="100%"', + 'height?: string|number="300px"', + 'hotkeyEnable?: bool', + 'hotkeys?: array', + 'lang?: string', + 'langs?: array', + 'nodeTeamplate?: string', + 'hSpace?: number', + 'vSpace?: number', + 'canvasPadding?: number', + 'removingNodeTip?: string', + 'lineCurvature?: number', + 'subLineWidth?: number', + 'lineColor?: string', + 'lineOpacity?: number', + 'lineSaturation?: number', + 'lineLightness?: number', + 'nodeLineWidth?: number', + 'showToggleButton?: bool', + 'readonly?: bool', + 'minimap?: bool', + 'toolbar?: bool', + 'zoom?: number', + 'zoomMax?: number', + 'zoomMin?: number', + 'minimapHeight?: number' + ); + + protected function build(): array + { + global $app; + + list($width, $height) = $this->prop(array('width', 'height', 'data')); + $dataVarName = "_mindmap_$this->gid"; + $mindmapPath = $app->getWebRoot() . 'js/mindmap/index.html?options=' . $dataVarName; + $options = $this->props->pick(array('hotkeyEnable', 'hotkeys', 'lang', 'langs', 'data', 'nodeTeamplate', 'hSpace', 'vSpace', 'canvasPadding', 'removingNodeTip', 'lineCurvature', 'subLineWidth', 'lineColor', 'lineOpacity', 'lineSaturation', 'lineLightness', 'nodeLineWidth', 'showToggleButton', 'readonly', 'minimap', 'toolbar', 'zoom', 'zoomMax', 'zoomMin', 'minimapHeight')); + return array + ( + h::iframe + ( + set::class('mindmap-iframe'), + set::src($mindmapPath), + set::allowfullscreen(true), + set::allowtransparency(true), + set::frameborder('no'), + set::scrolling('auto'), + set::style(array('width' => $width, 'height' => $height)), + set($this->getRestProps()) + ), + h::jsVar("window.$dataVarName", $options) + ); + } +} diff --git a/lib/zin/wg/modaldialog/css/v1.css b/lib/zin/wg/modaldialog/css/v1.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/zin/wg/modaldialog/v1.php b/lib/zin/wg/modaldialog/v1.php index b243d63c31..e6fe279941 100644 --- a/lib/zin/wg/modaldialog/v1.php +++ b/lib/zin/wg/modaldialog/v1.php @@ -91,6 +91,7 @@ class modalDialog extends wg return div ( setClass('modal-body', $this->prop('bodyClass')), + on::scroll('e.stopPropagation();'), set($this->prop('bodyProps')), $this->children(), $rawContent ? rawContent() : null, diff --git a/lib/zin/wg/modalheader/v1.php b/lib/zin/wg/modalheader/v1.php index 6fc5891c7b..6146626843 100644 --- a/lib/zin/wg/modalheader/v1.php +++ b/lib/zin/wg/modalheader/v1.php @@ -62,7 +62,8 @@ class modalHeader extends wg return h::div ( - set::class('modal-header panel-form rounded-md canvas mx-auto size-lg'), + set::class('modal-header panel-form rounded-md canvas mx-auto'), + set::style(array('margin-bottom' => '0px')), $header ); } diff --git a/lib/zin/wg/modalnextstep/css/v1.css b/lib/zin/wg/modalnextstep/css/v1.css new file mode 100644 index 0000000000..e8adc0d904 --- /dev/null +++ b/lib/zin/wg/modalnextstep/css/v1.css @@ -0,0 +1,3 @@ +.modal-next-step {width: min-content !important;} +.modal-next-step .modal-footer {padding: 0 32px 24px 32px; justify-content: center;} +.modal-next-step .modal-footer .toolbar {gap: 8px;} diff --git a/lib/zin/wg/modalnextstep/v1.php b/lib/zin/wg/modalnextstep/v1.php new file mode 100644 index 0000000000..7e47889ceb --- /dev/null +++ b/lib/zin/wg/modalnextstep/v1.php @@ -0,0 +1,43 @@ +prop('tip'); + $items = $this->prop('items'); + + return modalDialog + ( + setClass('modal-next-step'), + row + ( + set::align('center'), + setClass('gap-2'), + center + ( + setClass('w-8 h-8 rounded-full success'), + icon(setClass('text-xl font-bold'), 'check'), + ), + span + ( + setClass('font-medium text-md'), + $tip + ) + ), + set::footerActions($items), + ); + } +} diff --git a/lib/zin/wg/modulemenu/v1.php b/lib/zin/wg/modulemenu/v1.php index 5c532a1582..3fcfc360b6 100644 --- a/lib/zin/wg/modulemenu/v1.php +++ b/lib/zin/wg/modulemenu/v1.php @@ -5,6 +5,7 @@ namespace zin; class moduleMenu extends wg { private array $modules = array(); + private static array $filterMap = array(); protected static array $defineProps = array( 'modules: array', @@ -12,7 +13,8 @@ class moduleMenu extends wg 'settingLink?: string', 'closeLink: string', 'showDisplay?: bool=true', - 'allText?: string' + 'allText?: string', + 'title?: string' ); public static function getPageCSS(): string|false @@ -41,12 +43,22 @@ class moduleMenu extends wg else unset($item['items']); $parentItems[] = $item; } + return $parentItems; } private function getChildModule(int|string $id): array { - return array_filter($this->modules, fn($module) => $module->parent == $id); + return array_filter($this->modules, function($module) use($id) + { + /* Remove the rendered module. */ + if(isset(static::$filterMap["$module->parent-$module->id"])) return false; + + if($module->parent != $id) return false; + + static::$filterMap["$module->parent-$module->id"] = true; + return true; + }); } private function setMenuTreeProps(): void @@ -57,6 +69,8 @@ class moduleMenu extends wg private function getTitle(): string { + if($this->prop('title')) return $this->prop('title'); + global $lang; $activeKey = $this->prop('activeKey'); @@ -117,14 +131,17 @@ class moduleMenu extends wg ); } - private function buildCloseBtn(): ?wg + private function buildCloseBtn(): wg|null { + $closeLink = $this->prop('closeLink'); + if(!$closeLink) return null; + $activeKey = $this->prop('activeKey'); if(empty($activeKey)) return null; return a ( - set('href', $this->prop('closeLink')), + set('href', $closeLink), icon('close', setStyle('color', 'var(--color-slate-600)')) ); } diff --git a/lib/zin/wg/navigator/css/v1.css b/lib/zin/wg/navigator/css/v1.css new file mode 100644 index 0000000000..0add38f39f --- /dev/null +++ b/lib/zin/wg/navigator/css/v1.css @@ -0,0 +1,12 @@ +#navigator > li {position: relative; display: inline-block;} +#navigator > li > a {min-width: 100px;} +#navigator > li:first-child > a {border-radius: 4px 0 0 4px;} +#navigator > li:last-child > a {border-radius: 0 4px 4px 0;} +#navigator > li + li:before, +#navigator > li + li:after {content: ' '; width: 0; height: 0; border-style: solid; border-width: 15px 0 15px 15px; border-color: transparent transparent transparent #a6aab8; position: absolute; left: 0; top: 0;} +#navigator > li + li:after {border-left-color: rgba(var(--color-canvas-rgb),var(--tw-bg-opacity)); left: -1px;} +#navigator > li.active + li:after {border-left-color: rgba(var(--color-secondary-500-rgb),var(--tw-bg-opacity));} +#navigator > li.active:hover + li:after {border-left-color: rgba(var(--color-secondary-500-rgb));} +#navigator > li + li > a {padding-left: 35px;} +#navigator > li:hover {background-color: var(--zt-page-bg);} +#navigator > li:hover + li:after {border-left-color: var(--zt-page-bg);} diff --git a/lib/zin/wg/navigator/v1.php b/lib/zin/wg/navigator/v1.php new file mode 100644 index 0000000000..5548fed105 --- /dev/null +++ b/lib/zin/wg/navigator/v1.php @@ -0,0 +1,47 @@ +prop('items'); + if(!$items) return array(); + + $steps = array(); + foreach($items as $item) + { + $steps[] = li + ( + !empty($item->active) ? setClass('active') : null, + !empty($item->url) ? set::href($item->url) : null, + a + ( + setClass('btn shadow-none' . (!empty($item->active) ? ' secondary' : '')), + $item->text + ) + ); + } + return $steps; + } + + protected function build(): wg + { + return ul + ( + setID('navigator'), + setClass('nav nav-primary'), + $this->buildSteps() + ); + } +} diff --git a/lib/zin/wg/overviewblock/v1.php b/lib/zin/wg/overviewblock/v1.php index 9d61b10ff6..3920238dd3 100644 --- a/lib/zin/wg/overviewblock/v1.php +++ b/lib/zin/wg/overviewblock/v1.php @@ -47,7 +47,7 @@ class overviewBlock extends wg ), span ( - setClass('text-center'), + setClass('text-center text-sm'), $card->label ) ); @@ -78,14 +78,14 @@ class overviewBlock extends wg span ( set::title($bar->value), - setClass('block primary w-2'), + setClass('block primary bg-opacity-70 w-2'), setStyle(array('height' => $bar->rate)) ) ); $labels[] = span ( - setClass('text-center'), + setClass('text-center text-gray text-sm'), $bar->label ); } @@ -98,7 +98,7 @@ class overviewBlock extends wg setClass('basis-48'), span ( - setClass('mb-2'), + setClass('mb-3 text-sm text-gray'), $group->title ), div diff --git a/lib/zin/wg/pagebase/v1.php b/lib/zin/wg/pagebase/v1.php index 2b2c9f011e..9ebb5b502a 100644 --- a/lib/zin/wg/pagebase/v1.php +++ b/lib/zin/wg/pagebase/v1.php @@ -62,22 +62,24 @@ class pageBase extends wg $imports = context::current()->getImportList(); $webRoot = $app->getWebRoot(); $themeName = $app->cookie->theme; + $zuiPath = $config->zin->zuiPath; - $jsConfig->zin = true; + $jsConfig->zin = true; $headImports = array(); if($zui) { - $headImports[] = h::importCss($config->zin->zuiPath . 'zui.zentao.css', setID('zuiCSS')); - $headImports[] = h::importCss($config->zin->zuiPath . 'themes/' . $themeName . '.css', setID('zuiTheme')); - $headImports[] = h::importJs($config->zin->zuiPath . 'zui.zentao.umd.cjs', setID('zuiJS')); + $headImports[] = h::importCss($zuiPath . 'zui.zentao.css', setID('zuiCSS')); + $headImports[] = h::importCss($zuiPath . 'themes/' . $themeName . '.css', setID('zuiTheme')); + $headImports[] = h::importJs($zuiPath . 'zui.zentao.umd.cjs', setID('zuiJS')); + $headImports[] = h::jsCall('$.setLibRoot', $zuiPath); } $headImports[] = h::jsVar('window.config', $jsConfig, setID('configJS')); if($zui) $headImports[] = h::importJs($webRoot . 'js/zui3/zin.js', setID('zinJS')); if($config->debug) { - $js[] = h::createJsVarCode('window.zin', array('page' => $this->toJsonData(), 'definedProps' => wg::$definedPropsMap, 'wgBlockMap' => wg::$wgToBlockMap, 'config' => jsRaw('window.config'))); + $js[] = h::createJsVarCode('window.zin', array('page' => $this->toJSON(), 'definedProps' => wg::$definedPropsMap, 'wgBlockMap' => wg::$wgToBlockMap, 'config' => jsRaw('window.config'))); $js[] = 'console.log("[ZIN] ", window.zin);'; } else diff --git a/lib/zin/wg/pager/v1.php b/lib/zin/wg/pager/v1.php index bbd3147eaf..7497bdf5be 100644 --- a/lib/zin/wg/pager/v1.php +++ b/lib/zin/wg/pager/v1.php @@ -4,8 +4,51 @@ namespace zin; class pager extends wg { + protected static array $defineProps = array + ( + 'type?: string="full"', + 'page?: int', + 'recTotal?: int', + 'recPerPage?: int', + 'linkCreator?: string', + 'items?: array' + ); + + protected function buildProps(string $type = 'full'): void + { + global $lang; + $pager = data('pager'); + $pager->setParams(); + $params = $pager->params; + foreach($params as $key => $value) + { + if(strtolower($key) === 'recperpage') $params[$key] = '{recPerPage}'; + if(strtolower($key) === 'pageid') $params[$key] = '{page}'; + } + + $props = array(); + $props['page'] = $pager->pageID; + $props['recTotal'] = $pager->recTotal; + $props['recPerPage'] = $pager->recPerPage; + $props['linkCreator'] = createLink($pager->moduleName, $pager->methodName, $params); + $props['items'] = array + ( + $type == 'short' ? null : array('type' => 'info', 'text' => $lang->pager->totalCountAB), + $type == 'short' ? null : array('type' => 'size-menu', 'text' => $lang->pager->pageSizeAB), + array('type' => 'link', 'hint' => $lang->pager->firstPage, 'page' => 'first', 'icon' => 'icon-first-page'), + array('type' => 'link', 'hint' => $lang->pager->previousPage, 'page' => 'prev', 'icon' => 'icon-angle-left'), + array('type' => 'info', 'text' => '{page}/{pageTotal}'), + array('type' => 'link', 'hint' => $lang->pager->nextPage, 'page' => 'next', 'icon' => 'icon-angle-right'), + array('type' => 'link', 'hint' => $lang->pager->lastPage, 'page' => 'last', 'icon' => 'icon-last-page'), + ); + + $this->setProp($props); + } + protected function build(): zui { + $this->buildProps($this->prop('type')); + return zui::pager(inherit($this)); } } diff --git a/lib/zin/wg/panel/v1.php b/lib/zin/wg/panel/v1.php index ac92060751..6685c3edb7 100644 --- a/lib/zin/wg/panel/v1.php +++ b/lib/zin/wg/panel/v1.php @@ -5,9 +5,10 @@ namespace zin; class panel extends wg { protected static array $defineProps = array( - 'class?: string="rounded shadow ring-0 bg-canvas"', // 类名。 + 'class?: string="rounded ring-0 bg-canvas"', // 类名。 'size?: "sm"|"lg"', // 额外尺寸。 'title?: string', // 标题。 + 'shadow?: bool=true', // 阴影效果。 'titleClass?: string', // 标题类名。 'titleProps?: array', // 标题属性。 'headingClass?: string', // 标题栏类名。 @@ -53,7 +54,13 @@ class panel extends wg ( setClass('panel-heading', $this->prop('headingClass')), set($this->prop('headingProps')), - empty($title) ? null : div(setClass('panel-title', $this->prop('titleClass', empty($size) ? null : "text-$size")), $title, set($this->prop('titleProps'))), + !empty($title) ? div + ( + setClass('panel-title', $this->prop('titleClass', empty($size) ? null : "text-$size")), + $this->prop('titleIcon') ? icon($this->prop('titleIcon')) : null, + set($this->prop('titleProps')), + $title + ) : null, $headingBlock, $actions ); @@ -87,8 +94,8 @@ class panel extends wg protected function buildProps(): array { - list($class, $size) = $this->prop(array('class', 'size')); - return array(setClass('panel', $class, empty($size) ? null : "size-$size")); + list($class, $size, $shadow) = $this->prop(array('class', 'size', 'shadow')); + return array(setClass('panel', $class, empty($size) ? null : "size-$size", $shadow ? 'shadow' : null)); } protected function build(): wg diff --git a/lib/zin/wg/password/js/v1.js b/lib/zin/wg/password/js/v1.js new file mode 100644 index 0000000000..0b986374f5 --- /dev/null +++ b/lib/zin/wg/password/js/v1.js @@ -0,0 +1,52 @@ +function checkPassword() +{ + const password = $(event.target).val(); + const $strength = $(event.target).closest('.input-group').find('.' + strengthClass); + if(password == '') + { + $strength.html('').addClass('hidden'); + return false; + } + + const passwordStrength = passwordStrengthList[computePasswordStrength(password)]; + $strength.html(passwordStrength).removeClass('hidden'); +} + +function computePasswordStrength(password) +{ + if(password.length == 0) return 0; + + var strength = 0; + var length = password.length; + + var complexity = new Array(); + for(i = 0; i < length; i++) + { + letter = password.charAt(i); + var asc = letter.charCodeAt(); + if(asc >= 48 && asc <= 57) + { + complexity[0] = 1; + } + else if((asc >= 65 && asc <= 90)) + { + complexity[1] = 2; + } + else if(asc >= 97 && asc <= 122) + { + complexity[2] = 4; + } + else + { + complexity[3] = 8; + } + } + + var sumComplexity = 0; + for(i in complexity) sumComplexity += complexity[i]; + + if((sumComplexity == 7 || sumComplexity == 15) && password.length >= 6) strength = 1; + if(sumComplexity == 15 && password.length >= 10) strength = 2; + + return strength; +} diff --git a/lib/zin/wg/password/v1.php b/lib/zin/wg/password/v1.php new file mode 100644 index 0000000000..303a3559fe --- /dev/null +++ b/lib/zin/wg/password/v1.php @@ -0,0 +1,57 @@ +loadLang('user'); + $jsRoot = $app->getWebRoot() . 'js/'; + + list($id, $name, $checkStrength, $strengthID, $strengthClass) = $this->prop(array('id', 'name', 'checkStrength', 'strengthID', 'strengthClass')); + + return $checkStrength ? array + ( + h::jsCall('$.getLib', $jsRoot . 'md5.js'), + jsVar('window.strengthClass', $strengthClass), + jsVar('window.passwordStrengthList', $lang->user->passwordStrengthList), + inputGroup + ( + input + ( + setID($id), + on::keyup('checkPassword'), + set::type('password'), + set::name($name), + set::placeholder(zget($lang->user->placeholder->passwordStrength, $config->safe->mode, '')) + ), + span + ( + setID($strengthID), + setClass("input-group-addon {$strengthClass} hidden") + ) + ) + ) : input + ( + set::type('password'), + set::name($name), + set($this->getRestProps()), + ); + } +} diff --git a/lib/zin/wg/picker/v1.php b/lib/zin/wg/picker/v1.php index be9a5892d1..9c0939c3e9 100644 --- a/lib/zin/wg/picker/v1.php +++ b/lib/zin/wg/picker/v1.php @@ -98,7 +98,7 @@ class picker extends wg $pickerProps['items'] = $pickerItems; $pickerProps['defaultValue'] = $defaultValue; - if(!isset($pickerProps['emptyValue'])) $pickerProps['emptyValue'] = ($hasZeroValue || (!is_array($defaultValue) && "$defaultValue" !== '0')) ? '' : '0,'; + if(!isset($pickerProps['emptyValue'])) $pickerProps['emptyValue'] = ($hasZeroValue || "$defaultValue" !== '0') ? '' : '0,'; if(isset($pickerProps['id'])) { diff --git a/lib/zin/wg/roadmap/css/v1.css b/lib/zin/wg/roadmap/css/v1.css new file mode 100644 index 0000000000..4e73e9e834 --- /dev/null +++ b/lib/zin/wg/roadmap/css/v1.css @@ -0,0 +1,23 @@ +.release-head {width: 120px; border: 2px solid var(--color-gray-200); border-radius: 4px; margin-left: 30px; margin-bottom: 58px;} +.release-line > li {height: 156px;} +.release-line > li:nth-child(even) {align-items: start;} +.release-line > li:nth-child(odd) {align-items: end;} +.release-line > li:nth-child(even) > a {border-bottom: 4px solid var(--color-primary-200);} +.release-line > li:nth-child(odd) > a {border-top: 4px solid var(--color-primary-200);} +.release-line > li:last-child > a {border-color: transparent !important;} +.release-line > li > a {height: 80px; box-sizing: border-box;} +.release-line > li > a:before {display: block; content: ' '; width: 12px; height: 12px; background: #fff; border: 2px solid var(--color-success-500); border-radius: 50%; position: absolute;} +.release-line > li:nth-child(even) > a:before {bottom: -8px;} +.release-line > li:nth-child(odd) > a:before {top: -8px;} +.release-line > li:nth-child(odd) > a:after {top: 4px; left: 5px;} +.release-line > li:nth-child(even) > a:after {bottom: 4px; left: 5px;} +.release-line > li > a:after {position: absolute; display: block; width: 2px; height: 30px; content: ' '; background: var(--color-primary-200);} +.release-line > li:nth-child(odd) > a > div {margin-top: 36px;} +.release-line > li:nth-child(even) > a > div {margin-bottom: 36px;} +.release-link-line {pointer-events: none; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+YAAADiCAMAAAD0xy2eAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAABOUExURUdwTOXl5eXl5ebm5ufn5+rq6u7u7unp6eXl5f///+Xl5eXl5efn5+Xl5eXl5eXl5ebm5uXl5eXl5ebm5ubm5ubm5uXl5ebm5uXl5eXl5Uh6G7kAAAAZdFJOUwDuuVQtFQwg1gXH+Dpzoq1CmIV7aEuOX+MTWsN0AAAGrUlEQVQYGe3ACbaYOHRF0SuQkETfw5v/RPNdLpMZZKWez5buVQBcu8zmJACO1WOwcAqAZ7kx66MAeNa+ZncnAI51t1kvAK7F/hEAAAAAAACA/xP93gmAZ6tZOKsAODb2ZuUSAM/aYtZEAXCsTsFs7gTAse4eigD4lqMAAAAAAMD/D09/CYBrvVl/CYBjdXrN+lUAHOumYNavAuBYNwWzTQA866ZwCYBvnQAAAAD8V7RHEgDParHhzgLg2NqYDXMUAMfiPJg1qwA4lu/BbBMAz7rjvQTAtyoAAOBG1wmAb1O4swB4NptZcwmAY3EbzN4pCYBfaSpmwywAnl2NNQLgW44CAABuZAHwLQ1lSgLg2BjMhuUSAL9q25jZu2cB8CsfxcweAfBsnYcsAL51AgAAboxL2wmAZ5vZMF9VANxKZ29mYRsFwK98FDN7swA4Fvc3VAHwLQsAALgxlSMKgGeNmZU9CoBb9dqCmb33KABu1XV7zSwJgGfjPQsAALixHlEAXJvN3u3qBMCtdXvNbGjOLABuxaO3H1EAHEvPUgQAANwYlycLgGe7mZX76gTAq3wuwcyGZsoC4FUd997MLgHwLLVbJwB/j7UTANeyDf2+dgLgVuwHMxv6fa0C4FR37b39WATAse66+0kA/h45CYBvjZX5yQLg1xLsR1imsQqATzVOy2s/kgA4lp+tEYC/R9rOsRMAx1r7UebjygLgU57mMtgvswC4VeNzN++hT0oC4FDVZ7ehLPe55ioAPu3BfhseAXCqi+2xNa9d+rTnFTsB8KLqH7Xq09iPoTTzngXApXNrSrBfRn1i7ATAlS5e5570KWbD2y/b8XQC4NJSgv2W9RljrgLgR5fH9tznqk8ws1D6ZTs6AXCp6d/B/lH1Oc52jakKgBcpj+156FPtt1B6AXCp27elKcHMgj5d6Zdtn55rzALgRcpRn2x/vPrUMaYqAP9hVZ8a1/Y87rnpG32y/Qhv38y7ALiU+xIG+6Xok5r5Ps52jbkTABe6PF5Pq89ofxR9upg7AXAiXc+0b0tT3kaf0cyGtzTLdgqAS2N5B/tHo09ctn162jGmKgAedDmu7Xnp09ofjT5dTFUAnEjtedzz0pd30+cxs/D2zXy3AuBSW4L9tumzzvdxtmvMnQC40OXxes5Vn8n+2PTpkgC4EZ9p35amvMOuz2QWSr/M9zQKgEtTsH/t+qzH2a4xVQFwoaY4Xs+0r/rs9lsopwC4tO7b0pcwmJ367KVftn16rjEJgBddTvrM9sejT46pCoATKY7tedxz06/6zGYWSr9sexQAl+4S7LdLn3Z6rpirAHiR4tieR9ansX+E0o8C4NJzz015BzMb9bmX7TjbMXcC4EaXx06fYv8aogC4NLbnvi19CZb06Usz38dzjVkA/Kj6X8H+SPpUAfAjxfWZ9rnp36pPCKWZ7+m5ogC4VAf7o+rTCYAjKa7PdM9Nr0+1UJr5ntoxC4BLebB/BQFwKsX1me6lb/RJ89HGKgB+jfZLae5zFACX0nk3xX5pBMCvGttjnvRJWQB8myw0e5sFwK0p2C+h2aMAOJXbvQlm1gqAZ/k6sgD8PfpmXzsB8KuzX8rWZgFwKrV3P9iPIgB+1fFoQi8AziV9qgA4d4bljALg2GY/3vnJAuBVfubXfryrAPgVzyVYFgDfogD8Per8JAHwbDWzfh+rAHiVpmYws7CcWQC8qtddzOwWAM/yuawCAABu9HPbCYBj2cyG5swC4FacevtR9lEA3ErPEswWAfCsrncrAADgxraPAuBZN5i921oFwK31fs0szG0nAG7FvZhZLwCe5ak/BAAA3FizAPj2WjmyAPjVzcHMyhEFwK16zcHMyiQAftVrCzYLgGt1jQIAAG7MRxYAz7KZ9VMWALfqNQcz66ckAG7VdhnM7BYAx2q7DKcA+NZ1AgAAXtSyjQLg2WVm7x4FwK94v2ZWpiwAfo1bMLNJAByr1zyMAuBbJwAA4EYsUxYAzw4za55OANyq1zKYDctVBcCt7mnMLIwC4Fg+ytAJgG9ZAADAjWuPAuBaY9afSQD8GrdgNixXFQC3atuYWbirAPiVpmJFAHyLqwAAgBtjEgDX6jssVxUAv9IymIU7CoBfaSpm1p9JAPwat2A2C4BntW1WAQAAAPiPyGVKAuDZYTYslwD4VdvGzN49C4Bf6Shm1kQBcGydhyEJgGvdJQAA4EZOAuDbMiyrAHi2mNl7ZAHwKx2vmTVXFQC/1mUw6wXAszSVQwCcqwIAAG4cSQBcW4ckAK7lU/8D/a9fSZZlwxkAAAAASUVORK5CYII="); height: 196px; position: absolute; left: 10px; right: 100px; top: 97px; background-size: 100% 100%; background-repeat: no-repeat;} +.release-line .icon-flag {font-size: 24px;} +.release-line > li:nth-child(even) .icon-flag {bottom: -3px; left: 4px;} +.release-line > li:nth-child(odd) .icon-flag {left: 4px; top: -25px;} +.release-line > li:hover .title {color: var(--color-primary-500);} +.release-line > li:hover > a:before {border-color: var(--color-success-700);} +.release-line > li:hover > a:after {background: var(--color-primary-400);} diff --git a/lib/zin/wg/roadmap/js/v1.js b/lib/zin/wg/roadmap/js/v1.js new file mode 100644 index 0000000000..9fba3e1149 --- /dev/null +++ b/lib/zin/wg/roadmap/js/v1.js @@ -0,0 +1,67 @@ +$(function() +{ + // 修复连接曲线位置 + const fixReleasePathLine = function() + { + const $lines = $('.release-paths .release-line'); + $lines.each(function() + { + const $line = $(this); + const $next = $line.next(); + let $nextLine, $linkLine; + if($next.hasClass('release-line')) + { + $nextLine = $next; + } + else if($next.hasClass('release-link-line')) + { + $linkLine = $next; + $nextLine = $next.next(); + } + if ($nextLine && $nextLine.length) + { + if(!$linkLine) $linkLine = $('