v1.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <?php
  2. namespace zin;
  3. require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
  4. require_once dirname(__DIR__) . DS . 'backbtn' . DS . 'v1.php';
  5. require_once dirname(__DIR__) . DS . 'content' . DS . 'v1.php';
  6. require_once dirname(__DIR__) . DS . 'history' . DS . 'v1.php';
  7. require_once dirname(__DIR__) . DS . 'tabs' . DS . 'v1.php';
  8. require_once dirname(__DIR__) . DS . 'tabpane' . DS . 'v1.php';
  9. require_once dirname(__DIR__) . DS . 'entitytitle' . DS . 'v1.php';
  10. require_once dirname(__DIR__) . DS . 'toolbar' . DS . 'v1.php';
  11. class detail extends wg
  12. {
  13. /**
  14. * @var mixed[]
  15. */
  16. protected static $defineProps = array
  17. (
  18. /* 布局,如果为 `simple` 则适用于对话框。 */
  19. 'layout' => '?string',
  20. /* 返回按钮,可以为:`true` 显示默认的返回按钮,`false` 不显示返回按钮,或者指定返回按钮 backBtn 的 back 属性,或者通过数组指定 backBtn 所有属性。 */
  21. 'backBtn' => '?bool|string|array=true',
  22. /* 对象类型,例如 `story`、`task` 等,如果不指定则已当前的模块名称作为对象类型。 */
  23. 'objectType' => '?string',
  24. /* 对象 ID,如果不指定则尝试使用当前页面上的 `${$objectType}->id` 或者 `${$objectType}ID` 的值,例如 `$task->id` 或 `$taskID`。 */
  25. 'objectID' => '?int',
  26. /* 对象标题颜色。 */
  27. 'color' => '?string',
  28. /* 对象,如果不指定则尝试使用当前页面上的 `${$objectType}` 的值,例如 `$task`。 */
  29. 'object' => '?object',
  30. /* 是否已删除。 */
  31. 'deleted?: bool',
  32. /* 父级对象。 */
  33. 'parent' => '?object',
  34. /* 父级对象类型。 */
  35. 'parentType' => '?string',
  36. /* 父级对象 ID。 */
  37. 'parentID' => '?int|string=false',
  38. /* 父级标题。 */
  39. 'parentTitle' => '?string',
  40. /* 父级标题链接。 */
  41. 'parentUrl' => '?string',
  42. /* 父级标题属性。 */
  43. 'parentTitleProps' => '?array',
  44. /* 标题,如果不指定则尝试使用当前页面上的 `${$objectType}->title` 或 `${$objectType}->name` 的值,例如 `$story->title`、`$task->name` 。 */
  45. 'title' => '?string',
  46. /* 标题提示,如果不指定则使用title属性。 */
  47. 'titleProps' => '?string',
  48. /* 底部固定操作按钮的定义,不包括返回按钮,可以通过 `-` 来指定分割线,如果没有指定 actions 属性,则从 actionList 和 operateList 生成。 */
  49. 'actions' => '?array',
  50. /* 对象类型可用操作配置,如果没有指定 actionList 属性,则从 $config->$objectType->actionList 获取。 */
  51. 'actionList' => '?array',
  52. /* 对象类型当前操作配置,如果没有指定 operateList 属性,则从 $config->$objectType->$methoedName->actionList 获取。 */
  53. 'operateList' => '?array',
  54. /* 操作按钮链接格式化参数。 */
  55. 'urlFormatter' => '?array',
  56. /* 右上方工具栏的定义。 */
  57. 'toolbar' => '?array',
  58. /* 详情页的左侧主栏目内容区域,可以通过 `-` 来指定分割线,通过键名指定标题,通过 `html()` 来指定 HTML 内容,或者指定为 `callable` 或 `Closure` 动态生成内容,或者指定为 `content()` 属性。 */
  59. 'sections' => '?array',
  60. /* 详情页的右侧侧边栏标签页区域,可以通过 `-` 来指定分割线,通过键名指定标题,通过 `html()` 来指定 HTML 内容,或者指定为 `callable` 或 `Closure` 动态生成内容,或者指定为 `content()` 属性。 */
  61. 'tabs' => '?array',
  62. /* 详情页的右侧侧边栏宽度,如果不指定则默认为 `370`。 */
  63. 'sideWidth' => '?int=370',
  64. /* 详情页的左侧主栏目历史记录,如果设定为 `true` 显示当前对象默认的理智记录,否如果设置为 `false` 不显示历史记录,如果设置为数组则作为 `history()` 部件的属性来创建历史记录。 */
  65. 'history' => '?array|bool=true',
  66. /* 详情链接生成模版,例如 `/m=story&f=view&storyID={id}`,如果不指定则自动根据当前模块名和方法名生成。 */
  67. 'linkCreator' => '?string',
  68. /* 上一个对象按钮链接,也可以通过数组指定按钮的所有属性,如果指定为 true,则自动从 $preAndNext 对象上获取 ID 生成链接。 */
  69. 'prevBtn' => '?string|array|bool=true',
  70. /* 下一个对象按钮链接,也可以通过数组指定按钮的所有属性,如果指定为 true,则自动从 $preAndNext 对象上获取 ID 生成链接。 */
  71. 'nextBtn' => '?string|array|bool=true'
  72. );
  73. /**
  74. * @var mixed[]
  75. */
  76. protected static $defineBlocks = array
  77. (
  78. 'header' => array(),
  79. 'title' => array(),
  80. 'main' => array('map' => 'content,section'),
  81. 'sections' => array(),
  82. 'side' => array('map' => 'tabs'),
  83. 'actions' => array('map' => 'btn'),
  84. 'toolbar' => array('map' => 'btnGroup,toolbar')
  85. );
  86. public static function getPageJS()
  87. {
  88. return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
  89. }
  90. public static function getPageCSS()
  91. {
  92. return <<<'CSS'
  93. .detail-sections > * + * {margin-top: 16px}
  94. .detail-section.panel {--tw-ring-opacity: 0}
  95. .detail-section-title, .detail-section.panel .panel-heading {background: var(--color-canvas); position: sticky; top: 0; z-index: 2}
  96. .detail-section.panel .panel-heading {z-index: 1}
  97. .detail-section .detail-section .detail-section-title {z-index: 0}
  98. .detail-side > * + * {margin-top: 8px}
  99. .detail-side .tabs {padding: 12px 8px 12px 16px}
  100. .detail-side .tabs-header {position: sticky; top: 0;}
  101. .modal-dialog .detail-side .tabs-header {position: relative}
  102. .detail-side .tab-pane {padding: 0}
  103. .detail-sections .history-panel > .panel-heading,
  104. .detail-sections .history-panel > .panel-body {padding: 0.75rem 1.5rem}
  105. .detail-sections .history-panel-header > .listitem {padding: 0}
  106. .important-w-0 {width: 0!important;}
  107. .no-width {padding: 0!important; width: 0!important; overflow: hidden!important;}
  108. .detail-toggle {margin: 0!important;}
  109. .detail-toggle:hover {background-color: rgba(var(--color-border-rgb), var(--tw-bg-opacity)); transition-duration: .15s; transition-property: background-color; transition-timing-function: cubic-bezier(.4,0,.2,1);}
  110. .article > .files-list > li.file{white-space:normal;}
  111. .ai-task-status .status-doing {color: #FAAE1A;}
  112. .ai-task-status .status-done {color: #3883FA;}
  113. .ai-task-status .status-cancel, .ai-task-status .status-closed {color: #9EA3B0;}
  114. CSS;
  115. }
  116. protected function created()
  117. {
  118. global $app;
  119. $layout = $this->prop('layout');
  120. $objectType = $this->prop('objectType');
  121. $objectID = $this->prop('objectID');
  122. $object = $this->prop('object');
  123. if(is_null($layout))
  124. {
  125. $layout = isInModal() ? 'simple' : 'default';
  126. $this->setProp('layout', $layout);
  127. }
  128. if(!$objectType) $objectType = $app->rawModule;
  129. if(!$object) $object = data($objectType);
  130. if(!$objectID) $objectID = $object ? $object->id : data($objectType . 'ID');
  131. if(!$objectType || !$objectID || !$object)
  132. {
  133. $this->triggerError('The objectType, objectID or object property of widget "detail" is undefined.');
  134. }
  135. if(!$this->prop('objectType')) $this->setProp('objectType', $objectType);
  136. if(!$this->prop('objectID')) $this->setProp('objectID', $objectID);
  137. if(!$this->prop('object')) $this->setProp('object', $object);
  138. if($object)
  139. {
  140. if(!$this->hasProp('title')) $this->setProp('title', isset($object->name) ? $object->name : $object->title);
  141. if(!$this->hasProp('color') && isset($object->color)) $this->setProp('color', $object->color);
  142. if(!$this->hasProp('deleted') && isset($object->deleted)) $this->setProp('deleted', $object->deleted);
  143. }
  144. $parent = $this->prop('parent');
  145. if(!$parent && $object && isset($object->parent) && is_object($object->parent))
  146. {
  147. $parent = $object->parent;
  148. $this->setProp('parent', $parent);
  149. }
  150. if($parent)
  151. {
  152. if(!$this->hasProp('parentID')) $this->setProp('parentID', $parent->id);
  153. if(!$this->hasProp('parentTitle')) $this->setProp('parentTitle', isset($parent->name) ? $parent->name : $parent->title);
  154. if(!$this->hasProp('parentUrl')) $this->setProp('parentUrl', $parent->url);
  155. }
  156. if(!$this->hasProp('urlFormatter')) $this->setProp('urlFormatter', array('{id}' => $objectID));
  157. }
  158. /**
  159. * @param mixed[]|null $props
  160. */
  161. protected function buildBackBtn($props = null)
  162. {
  163. global $lang;
  164. $backBtn = $this->prop('backBtn');
  165. if($backBtn === false) return null;
  166. if(is_string($backBtn)) $backBtn = array('back' => $backBtn);
  167. elseif(!is_array($backBtn)) $backBtn = array();
  168. if($props) $backBtn = array_merge($backBtn, $props);
  169. return new backBtn
  170. (
  171. set::icon('back'),
  172. set::text($lang->goback),
  173. set::hint($lang->goback),
  174. setKey('backBtn'),
  175. set($backBtn)
  176. );
  177. }
  178. protected function buildTitle()
  179. {
  180. list($object, $objectID, $title, $titleProps, $color, $objectType, $parent, $parentID, $parentUrl, $parentTitle, $parentType, $parentTitleProps) = $this->prop(array('object', 'objectID', 'title', 'titleProps', 'color', 'objectType', 'parent', 'parentID', 'parentUrl', 'parentTitle', 'parentType', 'parentTitleProps'));
  181. $titleBlock = $this->block('title');
  182. $titleLeading = $this->block('titleLeading');
  183. return new entityTitle
  184. (
  185. setClass('min-w-0'),
  186. setKey('title'),
  187. set::id($objectID),
  188. set::object($object),
  189. set::title($title),
  190. set::titleClass('text-lg text-clip font-bold'),
  191. set::titleProps(array('title' => empty($titleProps) ? $title : $titleProps)),
  192. set::type($objectType),
  193. set::color($color),
  194. set::parentTitleClass('text-lg text-clip font-bold'),
  195. set::parent($parent),
  196. set::parentID($parentID),
  197. set::parentUrl($parentUrl),
  198. set::parentTitle($parentTitle),
  199. set::parentType($parentType),
  200. set::parentTitleProps($parentTitleProps),
  201. set::joinerClass('text-lg'),
  202. $titleLeading ? to::leading($titleLeading) : null,
  203. $titleBlock
  204. );
  205. }
  206. protected function buildToolbar()
  207. {
  208. $toolbar = $this->prop('toolbar');
  209. $toolbarBlock = $this->block('toolbar');
  210. if(!$toolbarBlock && !$toolbar) return null;
  211. return div
  212. (
  213. setClass('detail-toolbar'),
  214. setKey('toolbar'),
  215. $toolbar ? toolbar::create($toolbar, set::urlFormatter($this->prop('urlFormatter'))) : null,
  216. $toolbarBlock
  217. );
  218. }
  219. protected function buildHeader()
  220. {
  221. $isSimple = $this->prop('layout') === 'simple';
  222. return div
  223. (
  224. setClass('detail-header row gap-2 items-center flex-none'),
  225. setKey('header'),
  226. $isSimple ? null : $this->buildBackBtn(array('type' => 'primary-outline', 'class' => 'mr-2 size-md')),
  227. $this->buildTitle(),
  228. $this->block('header'),
  229. div(setClass('flex-auto')),
  230. $isSimple ? null : $this->buildToolbar()
  231. );
  232. }
  233. /**
  234. * @param mixed[]|callable|string|\zin\setting|\zin\node $item
  235. * @param string|null $title
  236. */
  237. protected function buildSection($item, $title = null)
  238. {
  239. if(is_callable($item)) $item = call_user_func($item, $title);
  240. elseif($item instanceof \Closure) $item = $item();
  241. if(is_null($title) && $item instanceof node) return $item;
  242. if($item instanceof setting) $item = $item->toArray();
  243. if(is_null($title) && isset($item['title']))
  244. {
  245. $title = $item['title'];
  246. unset($item['title']);
  247. }
  248. $titleActions = isset($item['titleActions']) ? $item['titleActions'] : null;
  249. $className = isset($item['className']) ? $item['className'] : null;
  250. if($titleActions) unset($item['titleActions']);
  251. return div
  252. (
  253. setClass('detail-section'),
  254. setKey($title),
  255. $title ? div
  256. (
  257. setClass('detail-section-title row items-center gap-2'),
  258. span(setClass('text-md py-1 font-bold'), $title),
  259. $titleActions ? toolbar::create($titleActions) : null
  260. ) : null,
  261. div(setClass("detail-section-content py-1 {$className}"), $item ? new content(is_array($item) ? set($item) : $item) : null)
  262. );
  263. }
  264. protected function buildMainSections()
  265. {
  266. global $app, $config;
  267. $sections = $this->prop('sections', []);
  268. if($config->edition != 'open' && empty($app->installing) && empty($app->upgrading)) $sections = $app->control->loadModel('flow')->buildExtendZinValue($sections, $this->prop('object'), 'info');
  269. $list = array();
  270. foreach($sections as $key => $item)
  271. {
  272. if($item === '-')
  273. {
  274. $list[] = hr();
  275. continue;
  276. }
  277. $list[] = $this->buildSection($item, is_string($key) ? $key : null);
  278. }
  279. return $list;
  280. }
  281. protected function buildHistory()
  282. {
  283. $history = $this->prop('history');
  284. if($history === false) return null;
  285. if(!is_array($history)) $history = array();
  286. return div
  287. (
  288. setClass('detail-sections canvas shadow rounded'),
  289. setKey('historyWrapper'),
  290. new history
  291. (
  292. set::className('detail-section overflow-visible'),
  293. set::objectType($this->prop('objectType')),
  294. set::objectID($this->prop('objectID')),
  295. set($history)
  296. )
  297. );
  298. }
  299. protected function buildActions()
  300. {
  301. global $app;
  302. $actions = $this->prop('actions');
  303. $actionsBlock = $this->block('actions');
  304. $isSimple = $this->prop('layout') === 'simple';
  305. if(!$actionsBlock && !is_array($actions)) return null;
  306. $toolbarProps = array_is_list($actions) ? array('items' => $actions) : $actions;
  307. if(!$isSimple)
  308. {
  309. $backBtn = $this->buildBackBtn(array('type' => 'ghost'));
  310. if(empty($toolbarProps['items'])) $toolbarProps['items'] = array($backBtn);
  311. else array_unshift($toolbarProps['items'], $backBtn, array('type' => 'divider'));
  312. }
  313. if(empty($toolbarProps['items']) && empty($actions)) return null;
  314. $toolbarClass = $app->getClientLang() == 'en' ? 'en-toolbar no-morph' : 'no-morph';
  315. return div
  316. (
  317. setClass('detail-actions center sticky mt-4 bottom-4 z-10'),
  318. setKey('actions'),
  319. div
  320. (
  321. setClass('bg-black text-fore-in-dark backdrop-blur bg-opacity-60 rounded p-1.5'),
  322. $toolbarProps ? toolbar
  323. (
  324. setClass($toolbarClass),
  325. set::urlFormatter($this->prop('urlFormatter')),
  326. set::btnType('ghost'),
  327. is_array($toolbarProps) ? set($toolbarProps) : null
  328. ) : null,
  329. $actionsBlock
  330. )
  331. );
  332. }
  333. protected function buildAITasks()
  334. {
  335. list($objectID, $objectType) = $this->prop(array('objectID', 'objectType'));
  336. if(!in_array($objectType, array('story', 'task', 'bug', 'testcase'))) return null;
  337. global $lang, $app, $config;
  338. if($config->edition == 'open') return null;
  339. $app->control->loadModel('aitask');
  340. jsVar('aiStatusList', $lang->aitask->statusList);
  341. if($objectType == 'testcase') $objectType = 'case';
  342. $users = $app->control->loadModel('user')->getPairs('noletter');
  343. $aiTasks = $app->control->aitask->getListByObject($objectType, $objectID);
  344. if(empty($aiTasks)) return null;
  345. $cols = $config->aitask->dtable->browse;
  346. unset($cols['objectType']);
  347. foreach($cols as $key => $col) $cols[$key]['sortType'] = false;
  348. $aiTasks = initTableData($aiTasks, $cols, $app->control->aitask);
  349. return panel
  350. (
  351. set::bodyClass('px-6 py-4'),
  352. div(setClass('text-md font-bold mb-2'), $lang->aitask->common),
  353. dtable(setID('aitasks'), set::cols($cols), set::data($aiTasks), set::userMap($users), set::onRenderCell(jsRaw('window.renderAITaskCell')), set::emptyTip($lang->aitask->noTask))
  354. );
  355. }
  356. protected function buildMain()
  357. {
  358. global $config;
  359. $mainSections = $this->buildMainSections();
  360. return div
  361. (
  362. setClass('detail-main flex-auto col gap-2 min-w-0'),
  363. $mainSections ? div
  364. (
  365. setClass('detail-sections canvas shadow rounded px-6 py-4'),
  366. setKey('main'),
  367. $mainSections,
  368. $this->block('main')
  369. ) : null,
  370. $this->block('sections'),
  371. !empty($config->enableAITeammate) && hasPriv('aitask', 'browse') ? $this->buildAITasks() : null,
  372. $this->children(),
  373. $this->buildHistory(),
  374. $this->buildActions()
  375. );
  376. }
  377. protected function buildTab($tab)
  378. {
  379. $title = isset($tab['title']) ? $tab['title'] : null;
  380. unset($tab['title']);
  381. return new tabPane
  382. (
  383. set::title($title),
  384. new content(set($tab))
  385. );
  386. }
  387. protected function buildTabs($group, $tabs)
  388. {
  389. $tabsView = new tabs
  390. (
  391. setClass('canvas rounded shadow'),
  392. setData('group', $group),
  393. setKey($group),
  394. set::collapse(true)
  395. );
  396. foreach($tabs as $tab)
  397. {
  398. $tabsView->add($this->buildTab($tab));
  399. }
  400. return $tabsView;
  401. }
  402. protected function buildTabsList()
  403. {
  404. global $app, $config;
  405. $tabs = $this->prop('tabs', []);
  406. if($config->edition != 'open' && empty($app->installing) && empty($app->upgrading)) $tabs = $app->control->loadModel('flow')->buildExtendZinValue($tabs, $this->prop('object'), 'basic');
  407. if(!$tabs) return null;
  408. $groups = array();
  409. foreach($tabs as $item)
  410. {
  411. $item = toArray($item);
  412. $group = isset($item['group']) ? $item['group'] : '';
  413. unset($item['group']);
  414. if(isset($groups[$group])) $groups[$group][] = $item;
  415. else $groups[$group] = array($item);
  416. }
  417. $views = array();
  418. foreach($groups as $groupName => $items)
  419. {
  420. $views[] = $this->buildTabs($groupName, $items);
  421. }
  422. return $views;
  423. }
  424. protected function buildSide()
  425. {
  426. return div
  427. (
  428. setClass('detail-side flex-none relative'),
  429. setStyle('width', $this->prop('sideWidth') . 'px'),
  430. setKey('side'),
  431. $this->buildTabsList(),
  432. $this->block('side'),
  433. div(
  434. setClass('detail-toggle h-full w-2 absolute top-0 flex justify-center items-center'),
  435. setStyle('left', '-.5rem'),
  436. btn(
  437. setClass('w-4 rounded-lg'),
  438. set::icon('chevron-right'),
  439. set::iconClass('text-sm text-gray'),
  440. on::click()
  441. ->do(<<<'JS'
  442. $('.detail-side').toggleClass('important-w-0');
  443. $('.tabs').toggleClass('no-width');
  444. $element.find('.icon')
  445. .toggleClass('icon-chevron-right')
  446. .toggleClass('icon-chevron-left');
  447. JS
  448. )
  449. )
  450. )
  451. );
  452. }
  453. protected function buildBody()
  454. {
  455. return div
  456. (
  457. setClass('detail-body row gap-2 items-start'),
  458. setKey('body'),
  459. $this->buildMain(),
  460. $this->buildSide()
  461. );
  462. }
  463. protected function buildPrevAndNext()
  464. {
  465. if($this->prop('layout') === 'simple') return null;
  466. list($linkCreator, $prevBtn, $nextBtn, $objectType) = $this->prop(array('linkCreator', 'prevBtn', 'nextBtn', 'objectType'));
  467. $preAndNext = data('preAndNext');
  468. $idKey = isset($preAndNext->idKey) ? $preAndNext->idKey : 'id';
  469. global $app;
  470. if(!$linkCreator && $preAndNext && ($prevBtn === true || $nextBtn === true))
  471. {
  472. $linkCreator = createLink($app->rawModule, $app->rawMethod, $objectType . 'ID={id}');
  473. }
  474. if($prevBtn === true && $preAndNext && $preAndNext->pre && $linkCreator)
  475. {
  476. $prevBtn = array();
  477. $objectID = $preAndNext->pre->$idKey;
  478. $prevBtn['url'] = str_replace('{id}', "{$objectID}", $linkCreator);
  479. $prevBtn['hint'] = "#{$objectID} " . (isset($preAndNext->pre->title) ? $preAndNext->pre->title : $preAndNext->pre->name);
  480. }
  481. elseif(is_string($prevBtn))
  482. {
  483. $prevBtn = array('url' => $prevBtn);
  484. }
  485. if($nextBtn === true && $preAndNext && $preAndNext->next && $linkCreator)
  486. {
  487. $nextBtn = array();
  488. $objectID = $preAndNext->next->$idKey;
  489. $nextBtn['url'] = str_replace('{id}', "{$objectID}", $linkCreator);
  490. $nextBtn['hint'] = "#{$objectID} " . (isset($preAndNext->next->title) ? $preAndNext->next->title : $preAndNext->next->name);
  491. }
  492. elseif(is_string($nextBtn))
  493. {
  494. $nextBtn = array('url' => $nextBtn);
  495. }
  496. $buttons = array();
  497. if(is_array($prevBtn))
  498. {
  499. $buttons[] = new btn
  500. (
  501. setClass('detail-prev-btn absolute top-0 left-0 inverse rounded-full w-12 h-12 center bg-opacity-40 backdrop-blur ring-0'),
  502. set::icon('angle-left icon-2x text-canvas'),
  503. setData('app', $app->tab),
  504. set($prevBtn)
  505. );
  506. }
  507. if(is_array($nextBtn))
  508. {
  509. $buttons[] = new btn
  510. (
  511. setClass('detail-next-btn absolute top-0 right-0 inverse rounded-full w-12 h-12 center bg-opacity-40 backdrop-blur ring-0'),
  512. set::icon('angle-right icon-2x text-canvas'),
  513. setData('app', $app->tab),
  514. set($nextBtn)
  515. );
  516. }
  517. if(!$buttons) return null;
  518. return div
  519. (
  520. setClass('detail-prev-next fixed top-0 left-0 bottom-0 right-0 z-10 pointer-events-none'),
  521. div
  522. (
  523. setClass('container relative pointer-events-auto'),
  524. setStyle(array('top' => '50%', 'margin' => '-24px auto auto')),
  525. $buttons
  526. )
  527. );
  528. }
  529. protected function build()
  530. {
  531. global $app;
  532. list($objectType, $objectID, $layout) = $this->prop(array('objectType', 'objectID', 'layout'));
  533. return div
  534. (
  535. setClass('detail-view col relative gap-2.5'),
  536. setData('id', $objectID),
  537. setData('type', $objectType),
  538. $this->buildHeader(),
  539. $this->buildBody(),
  540. $layout === 'simple' ? null : $this->buildPrevAndNext(),
  541. html($app->control->appendExtendCssAndJS('', '', $this->prop('object'))),
  542. js
  543. (
  544. 'const $doc = $(document)',
  545. 'const binddedKey = "zt.detail.bindded"',
  546. 'if($doc.data(binddedKey)) return',
  547. '$doc.on("keyup.detail.zt", e => {',
  548. 'if($(".modal.show").length) return',
  549. 'const $target = $(e.target);',
  550. 'if($target.is("input,textarea,select,[contenteditable]")) return',
  551. 'let $btn = null',
  552. 'if(e.keyCode === 37) $btn = $(".detail-prev-btn")',
  553. 'else if(e.keyCode === 39) $btn = $(".detail-next-btn")',
  554. 'if($btn && $btn.length) $btn[0].click()',
  555. '}).data(binddedKey, true)',
  556. '$doc.one("pageunmount.app", () => {$(document).off("keyup.detail.zt").removeData(binddedKey)})'
  557. )
  558. );
  559. }
  560. }