cabloy 5.1.46 → 5.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.47
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fix web header navigation rendering.
8
+ - Prevent web header dropdown menus from being clipped.
9
+
10
+ ### Improvements
11
+
12
+ - Update internal project dependencies and maintenance changes.
13
+
3
14
  ## 5.1.46
4
15
 
5
16
  ### Improvements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.46",
3
+ "version": "5.1.47",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -9,6 +9,7 @@ export interface ControllerItemLinkProps {
9
9
  icon?: keyof IIconRecord;
10
10
  href?: string;
11
11
  to?: string | object;
12
+ target?: string;
12
13
  }
13
14
 
14
15
  @Controller()
@@ -16,6 +17,7 @@ export class ControllerItemLink extends BeanControllerBase {
16
17
  static $propsDefault = {
17
18
  description: '',
18
19
  icon: '',
20
+ target: '_blank',
19
21
  };
20
22
 
21
23
  _renderLink() {
@@ -28,7 +30,7 @@ export class ControllerItemLink extends BeanControllerBase {
28
30
  ];
29
31
  if (this.$props.href) {
30
32
  return (
31
- <a href={this.$props.href} target="_blank">
33
+ <a href={this.$props.href} target={this.$props.target}>
32
34
  {domContent}
33
35
  </a>
34
36
  );
@@ -24,7 +24,7 @@ export class RenderHeader extends BeanRenderBase {
24
24
  </button>
25
25
  </div>
26
26
  <div class="text-xl px-4">{this.sys.env.APP_TITLE}</div>
27
- <div class="mx-2 flex-2 px-2">{this.$$r.$$renderTabs.renderTabs()}</div>
27
+ <div class="mx-2 min-w-0 flex-1 px-2">{this.$$r.$$renderTabs.renderTabs()}</div>
28
28
  <div class="hidden flex-none lg:block">
29
29
  <ul class="menu menu-horizontal">
30
30
  {this.$$r.$$renderLocale.render()}
@@ -1,11 +1,15 @@
1
1
  import type { VNode } from 'vue';
2
- import type { RouteTab } from 'zova-module-a-routertabs';
3
2
 
3
+ import { RouterLink } from '@cabloy/vue-router';
4
4
  import { BeanRenderBase, ClientOnly } from 'zova';
5
5
  import { Render } from 'zova-module-a-bean';
6
- import { $iconName, ZIcon } from 'zova-module-a-icon';
6
+ import { ZIcon } from 'zova-module-a-icon';
7
7
  import { ZRouterViewTabs } from 'zova-module-a-routertabs';
8
- import { closeNearestDetails, ZItemLink } from 'zova-module-home-base';
8
+ import { closeNearestDetails } from 'zova-module-home-base';
9
+
10
+ import type { TypeMenuGroup, TypeMenuItem } from '../../model/menu.js';
11
+
12
+ type TypeMenuLeaf = Exclude<TypeMenuItem, TypeMenuGroup>;
9
13
 
10
14
  @Render()
11
15
  export class RenderTabs extends BeanRenderBase {
@@ -13,84 +17,171 @@ export class RenderTabs extends BeanRenderBase {
13
17
  const $$modelTabs = this.$$modelTabs;
14
18
  if (!$$modelTabs) return;
15
19
 
16
- const domTabs = $$modelTabs.tabs.map(tab => this._renderTab(tab));
20
+ const domTabs = $$modelTabs.tabs.map(tab => {
21
+ return this._renderMenuItem(tab.info as TypeMenuItem, true, tab.tabKey);
22
+ });
17
23
  const domWrapper = (
18
- <div role="tablist" class="tabs tabs-lifted">
19
- {domTabs}
20
- </div>
24
+ <ul class="menu menu-horizontal w-max min-w-full flex-nowrap gap-1 px-0">{domTabs}</ul>
21
25
  );
22
26
  if (!$$modelTabs.cache) return domWrapper;
23
27
  return <ClientOnly>{domWrapper}</ClientOnly>;
24
28
  }
25
29
 
26
- private _renderTab(tab: RouteTab): VNode {
27
- const $$modelTabs = this.$$modelTabs;
28
- const { tabKey, info } = tab;
29
- const titleLocale = info?.title || '';
30
- const tabIcon = this.getTabIcon(tab);
31
- if (info.folder) {
30
+ private _renderMenuItem(item: TypeMenuItem, topLevel: boolean = false, tabKey?: string): VNode {
31
+ if (item.folder) {
32
+ return this._renderMenuFolder(item, topLevel);
33
+ }
34
+ if (item.separator) {
35
+ return this._renderMenuSeparator(item, topLevel);
36
+ }
37
+ return this._renderMenuLeaf(item, topLevel, tabKey);
38
+ }
39
+
40
+ private _renderMenuFolder(item: TypeMenuGroup, topLevel: boolean): VNode {
41
+ const isActive = this._hasActiveDescendant(item);
42
+ const className = this._getMenuItemClassName(isActive);
43
+ const title = item.title ?? '';
44
+ const childrenClass = topLevel ? 'w-56' : 'w-52';
45
+ return (
46
+ <li key={this._getMenuItemKey(item)}>
47
+ <details>
48
+ <summary class={className}>
49
+ {this._renderItemIcon(item.icon as any)}
50
+ <span>{title}</span>
51
+ </summary>
52
+ <ul class={`bg-base-100 rounded-t-none p-2 shadow ${childrenClass}`}>
53
+ {item.children.map(child => this._renderMenuItem(child))}
54
+ </ul>
55
+ </details>
56
+ </li>
57
+ );
58
+ }
59
+
60
+ private _renderMenuLeaf(item: TypeMenuLeaf, topLevel: boolean, tabKey?: string): VNode {
61
+ const title = item.title ?? '';
62
+ const key = this._getMenuItemKey(item);
63
+ const className = this._getMenuItemClassName(this._isMenuLeafActive(item, tabKey));
64
+ const domContent = this._renderMenuItemContent(item.icon as any, title);
65
+ const onClick = topLevel ? undefined : closeNearestDetails;
66
+
67
+ if (item.external) {
32
68
  return (
33
- <li>
34
- <details>
35
- <summary>
36
- {!!tabIcon && <ZIcon name={tabIcon as any} width="24"></ZIcon>}
37
- {titleLocale}
38
- </summary>
39
- <ClientOnly>
40
- <ul class="bg-base-100 rounded-t-none p-2 w-48">
41
- {info.children
42
- ?.filter(item => !item.folder)
43
- .map(item => (
44
- <li key={item.link} onClick={closeNearestDetails}>
45
- <ZItemLink
46
- title={item.title!}
47
- icon={(item.icon as any) ?? $iconName('::none')}
48
- href={item.link && item.external ? item.link : undefined}
49
- to={item.link && !item.external ? item.link : undefined}
50
- ></ZItemLink>
51
- </li>
52
- ))}
53
- </ul>
54
- </ClientOnly>
55
- </details>
69
+ <li key={key} onClick={onClick}>
70
+ <a class={className} href={item.link} target={item.target ?? '_blank'}>
71
+ {domContent}
72
+ </a>
56
73
  </li>
57
74
  );
58
75
  }
59
- // not external
60
- if (!info.external) {
61
- const className = tabKey === $$modelTabs.tabKeyCurrent ? 'text-primary' : '';
62
76
 
77
+ if (topLevel && tabKey) {
63
78
  return (
64
- <a
65
- key={tabKey}
66
- role="tab"
67
- class={`tab ${className}`}
68
- onClick={() => {
69
- $$modelTabs.activeTab(tabKey);
70
- }}
71
- >
72
- {!!tabIcon && <ZIcon name={tabIcon as any} width="24"></ZIcon>}
73
- {titleLocale}
74
- </a>
79
+ <li key={key}>
80
+ <a
81
+ class={className}
82
+ onClick={() => {
83
+ void this.$$modelTabs.activeTab(tabKey);
84
+ }}
85
+ >
86
+ {domContent}
87
+ </a>
88
+ </li>
75
89
  );
76
90
  }
77
- // external
91
+
92
+ return (
93
+ <li key={key} onClick={onClick}>
94
+ <RouterLink class={className} to={this._buildMenuTo(item)}>
95
+ {domContent}
96
+ </RouterLink>
97
+ </li>
98
+ );
99
+ }
100
+
101
+ private _renderMenuSeparator(item: TypeMenuLeaf, topLevel: boolean): VNode {
102
+ const key = `${this._getMenuItemKey(item)}:separator`;
103
+ if (topLevel) {
104
+ return (
105
+ <li key={key} class="mx-1 self-center opacity-30">
106
+ <span class="pointer-events-none px-0">|</span>
107
+ </li>
108
+ );
109
+ }
110
+ return <li key={key} class="menu-disabled my-1 h-px bg-base-300"></li>;
111
+ }
112
+
113
+ private _buildMenuTo(item: TypeMenuLeaf) {
114
+ let to: any;
115
+ if (!item.external) {
116
+ to = {};
117
+ if (this.$router.isRouterName(item.link)) {
118
+ to.name = item.link;
119
+ } else {
120
+ to.path = item.link;
121
+ }
122
+ if (item.meta?.params && to.name) {
123
+ to.params = item.meta.params;
124
+ }
125
+ if (item.meta?.query) {
126
+ to.query = item.meta.query;
127
+ }
128
+ }
129
+ return to;
130
+ }
131
+
132
+ private _hasActiveDescendant(item: TypeMenuGroup): boolean {
133
+ return item.children.some(child => {
134
+ if (child.folder) return this._hasActiveDescendant(child);
135
+ return this._isMenuLeafCurrent(child);
136
+ });
137
+ }
138
+
139
+ private _isMenuLeafActive(item: TypeMenuLeaf, tabKey?: string): boolean {
140
+ if (item.external || !item.link) return false;
141
+ if (tabKey && tabKey === this.$$modelTabs.tabKeyCurrent) return true;
142
+ return this._isMenuLeafCurrent(item);
143
+ }
144
+
145
+ private _isMenuLeafCurrent(item: TypeMenuLeaf): boolean {
146
+ if (item.external || !item.link) return false;
147
+ const currentRoute = this.$currentRoute;
148
+ if (!currentRoute) return false;
149
+ const fullPath = this.$router.isRouterName(item.link)
150
+ ? this.$router.resolveName(
151
+ item.link as never,
152
+ {
153
+ params: item.meta?.params,
154
+ query: item.meta?.query,
155
+ } as never,
156
+ )
157
+ : this.$router.resolvePath(item.link as never, item.meta?.query as never);
158
+ return currentRoute.fullPath === fullPath;
159
+ }
160
+
161
+ private _getMenuItemKey(item: TypeMenuItem): string {
162
+ if (item.folder) return item.name || item.title || '';
163
+ return item.name || item.link || item.title || '';
164
+ }
165
+
166
+ private _getMenuItemClassName(isActive: boolean): string {
167
+ return isActive ? 'active text-primary' : '';
168
+ }
169
+
170
+ private _renderMenuItemContent(icon: string | undefined, title: string): VNode {
78
171
  return (
79
- <ZItemLink
80
- key={tabKey}
81
- title={titleLocale}
82
- icon={info.icon as any}
83
- href={info.link}
84
- ></ZItemLink>
172
+ <>
173
+ {this._renderItemIcon(icon)}
174
+ <span>{title}</span>
175
+ </>
85
176
  );
86
177
  }
87
178
 
88
- public getTabIcon(tab: RouteTab) {
89
- const { info } = tab;
90
- return info?.icon ? info?.icon : '';
179
+ private _renderItemIcon(icon?: string): VNode | undefined {
180
+ if (!icon) return;
181
+ return <ZIcon name={icon as any} width="20" height="20"></ZIcon>;
91
182
  }
92
183
 
93
- _renderRouterViewTabs() {
184
+ public _renderRouterViewTabs(): VNode {
94
185
  return <ZRouterViewTabs></ZRouterViewTabs>;
95
186
  }
96
187
  }