@logicflow/react-node-registry 1.2.0-alpha.2 → 1.2.0-alpha.4
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +15 -0
- package/dist/index.css +122 -0
- package/es/components/Container.d.ts +9 -0
- package/es/components/Container.js +16 -0
- package/es/components/Container.js.map +1 -0
- package/es/components/TitleBar.d.ts +8 -0
- package/es/components/TitleBar.js +87 -0
- package/es/components/TitleBar.js.map +1 -0
- package/es/index.css +122 -0
- package/es/index.less +1 -0
- package/es/model.d.ts +18 -1
- package/es/model.js +46 -8
- package/es/model.js.map +1 -1
- package/es/style/index.css +122 -0
- package/es/style/index.less +140 -0
- package/es/style/raw.d.ts +4 -0
- package/es/style/raw.js +6 -0
- package/es/style/raw.js.map +1 -0
- package/es/view.d.ts +10 -1
- package/es/view.js +123 -9
- package/es/view.js.map +1 -1
- package/es/wrapper.d.ts +1 -1
- package/es/wrapper.js +4 -2
- package/es/wrapper.js.map +1 -1
- package/lib/components/Container.d.ts +9 -0
- package/lib/components/Container.js +23 -0
- package/lib/components/Container.js.map +1 -0
- package/lib/components/TitleBar.d.ts +8 -0
- package/lib/components/TitleBar.js +114 -0
- package/lib/components/TitleBar.js.map +1 -0
- package/lib/index.css +122 -0
- package/lib/index.less +1 -0
- package/lib/model.d.ts +18 -1
- package/lib/model.js +44 -6
- package/lib/model.js.map +1 -1
- package/lib/style/index.css +122 -0
- package/lib/style/index.less +140 -0
- package/lib/style/raw.d.ts +4 -0
- package/lib/style/raw.js +9 -0
- package/lib/style/raw.js.map +1 -0
- package/lib/view.d.ts +10 -1
- package/lib/view.js +123 -9
- package/lib/view.js.map +1 -1
- package/lib/wrapper.d.ts +1 -1
- package/lib/wrapper.js +7 -2
- package/lib/wrapper.js.map +1 -1
- package/package.json +3 -3
- package/rollup.config.js +52 -0
- package/src/components/Container.tsx +33 -0
- package/src/components/TitleBar.tsx +149 -0
- package/src/index.less +1 -0
- package/src/model.ts +82 -6
- package/src/style/index.less +140 -0
- package/src/style/raw.ts +129 -0
- package/src/view.ts +102 -9
- package/src/wrapper.tsx +17 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.lf-vue-node-container {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
padding: 6px;
|
|
7
|
+
color: #474747;
|
|
8
|
+
border-radius: 12px;
|
|
9
|
+
box-shadow: 0 0 10px #cad2e15f;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lf-vue-node-content-wrap {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex: 1 1 auto;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.lf-vue-node-title {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: flex-start;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
margin-bottom: 4px;
|
|
24
|
+
padding: 0 8px;
|
|
25
|
+
backdrop-filter: saturate(180%) blur(4px);
|
|
26
|
+
|
|
27
|
+
&-expanded {
|
|
28
|
+
margin-bottom: 6px;
|
|
29
|
+
padding-bottom: 8px;
|
|
30
|
+
border-bottom: 1px solid #eaeaea;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
35
|
+
.lf-vue-node-title {
|
|
36
|
+
backdrop-filter: none;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.lf-vue-node-title-left {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 6px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.lf-vue-node-title-icon {
|
|
48
|
+
display: inline-block;
|
|
49
|
+
width: 16px;
|
|
50
|
+
height: 16px;
|
|
51
|
+
color: #666;
|
|
52
|
+
font-style: normal;
|
|
53
|
+
line-height: 16px;
|
|
54
|
+
text-align: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.lf-vue-node-title-text {
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
color: #333;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
text-overflow: ellipsis;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.lf-vue-node-title-actions {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
align-items: center;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.lf-vue-node-title-expand,
|
|
73
|
+
.lf-vue-node-title-more {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
width: 20px;
|
|
78
|
+
height: 20px;
|
|
79
|
+
padding: 2px;
|
|
80
|
+
background: transparent;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: background 0.15s ease;
|
|
85
|
+
appearance: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.lf-vue-node-title-expand:hover,
|
|
89
|
+
.lf-vue-node-title-more:hover {
|
|
90
|
+
background: rgb(0 0 0 / 6%);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.lf-vue-node-title-expand-icon {
|
|
94
|
+
color: #666;
|
|
95
|
+
font-style: normal;
|
|
96
|
+
transition: transform 0.3s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.lf-vue-node-title-more-icon {
|
|
100
|
+
color: #666;
|
|
101
|
+
font-style: normal;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.lf-vue-node-title-tooltip {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: -50px;
|
|
107
|
+
right: -135px;
|
|
108
|
+
min-width: 120px;
|
|
109
|
+
max-width: 240px;
|
|
110
|
+
padding: 6px 8px;
|
|
111
|
+
background: #fff;
|
|
112
|
+
border: 1px solid rgb(0 0 0 / 10%);
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 12%);
|
|
115
|
+
transform: translateY(calc(100% + 4px));
|
|
116
|
+
transition:
|
|
117
|
+
opacity 0.15s ease,
|
|
118
|
+
transform 0.15s ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.lf-vue-node-title-tooltip-list {
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: 4px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.lf-vue-node-title-tooltip-item {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: flex-start;
|
|
131
|
+
padding: 6px;
|
|
132
|
+
color: #333;
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.lf-vue-node-title-tooltip-item:hover {
|
|
139
|
+
background: rgb(0 0 0 / 5%);
|
|
140
|
+
}
|
package/src/style/raw.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto generated file, do not modify it!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const content = `.lf-vue-node-container {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
padding: 6px;
|
|
13
|
+
color: #474747;
|
|
14
|
+
border-radius: 12px;
|
|
15
|
+
box-shadow: 0 0 10px #cad2e15f;
|
|
16
|
+
}
|
|
17
|
+
.lf-vue-node-content-wrap {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex: 1 1 auto;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
}
|
|
22
|
+
.lf-vue-node-title {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: flex-start;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
margin-bottom: 4px;
|
|
28
|
+
padding: 0 8px;
|
|
29
|
+
backdrop-filter: saturate(180%) blur(4px);
|
|
30
|
+
}
|
|
31
|
+
.lf-vue-node-title-expanded {
|
|
32
|
+
margin-bottom: 6px;
|
|
33
|
+
padding-bottom: 8px;
|
|
34
|
+
border-bottom: 1px solid #eaeaea;
|
|
35
|
+
}
|
|
36
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
37
|
+
.lf-vue-node-title {
|
|
38
|
+
backdrop-filter: none;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
.lf-vue-node-title-left {
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: 6px;
|
|
44
|
+
align-items: center;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
.lf-vue-node-title-icon {
|
|
48
|
+
display: inline-block;
|
|
49
|
+
width: 16px;
|
|
50
|
+
height: 16px;
|
|
51
|
+
color: #666;
|
|
52
|
+
font-style: normal;
|
|
53
|
+
line-height: 16px;
|
|
54
|
+
text-align: center;
|
|
55
|
+
}
|
|
56
|
+
.lf-vue-node-title-text {
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
color: #333;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
text-overflow: ellipsis;
|
|
63
|
+
}
|
|
64
|
+
.lf-vue-node-title-actions {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
align-items: center;
|
|
68
|
+
}
|
|
69
|
+
.lf-vue-node-title-expand,
|
|
70
|
+
.lf-vue-node-title-more {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
width: 20px;
|
|
75
|
+
height: 20px;
|
|
76
|
+
padding: 2px;
|
|
77
|
+
background: transparent;
|
|
78
|
+
border: none;
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
transition: background 0.15s ease;
|
|
82
|
+
appearance: none;
|
|
83
|
+
}
|
|
84
|
+
.lf-vue-node-title-expand:hover,
|
|
85
|
+
.lf-vue-node-title-more:hover {
|
|
86
|
+
background: rgba(0, 0, 0, 0.06);
|
|
87
|
+
}
|
|
88
|
+
.lf-vue-node-title-expand-icon {
|
|
89
|
+
color: #666;
|
|
90
|
+
font-style: normal;
|
|
91
|
+
transition: transform 0.3s ease;
|
|
92
|
+
}
|
|
93
|
+
.lf-vue-node-title-more-icon {
|
|
94
|
+
color: #666;
|
|
95
|
+
font-style: normal;
|
|
96
|
+
}
|
|
97
|
+
.lf-vue-node-title-tooltip {
|
|
98
|
+
position: absolute;
|
|
99
|
+
top: -50px;
|
|
100
|
+
right: -135px;
|
|
101
|
+
min-width: 120px;
|
|
102
|
+
max-width: 240px;
|
|
103
|
+
padding: 6px 8px;
|
|
104
|
+
background: #fff;
|
|
105
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
|
108
|
+
transform: translateY(calc(100% + 4px));
|
|
109
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
110
|
+
}
|
|
111
|
+
.lf-vue-node-title-tooltip-list {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
gap: 4px;
|
|
115
|
+
}
|
|
116
|
+
.lf-vue-node-title-tooltip-item {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: flex-start;
|
|
120
|
+
padding: 6px;
|
|
121
|
+
color: #333;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
border-radius: 4px;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
.lf-vue-node-title-tooltip-item:hover {
|
|
127
|
+
background: rgba(0, 0, 0, 0.05);
|
|
128
|
+
}
|
|
129
|
+
`
|
package/src/view.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { createElement, ReactPortal } from 'react'
|
|
2
2
|
import { createRoot, Root } from 'react-dom/client'
|
|
3
3
|
import { HtmlNode } from '@logicflow/core'
|
|
4
|
+
import {
|
|
5
|
+
throttle,
|
|
6
|
+
round,
|
|
7
|
+
get,
|
|
8
|
+
isFunction,
|
|
9
|
+
isArray,
|
|
10
|
+
clamp,
|
|
11
|
+
isNumber,
|
|
12
|
+
} from 'lodash-es'
|
|
4
13
|
import { Wrapper } from './wrapper'
|
|
5
14
|
import { Portal } from './portal'
|
|
6
15
|
import { createPortal } from 'react-dom'
|
|
7
16
|
|
|
8
17
|
export class ReactNodeView extends HtmlNode {
|
|
9
18
|
root?: Root
|
|
19
|
+
private containerEl?: HTMLElement
|
|
20
|
+
private __resizeObserver?: ResizeObserver
|
|
21
|
+
private __resizeRafId?: number
|
|
22
|
+
private __lastWidth?: number
|
|
23
|
+
private __lastHeight?: number
|
|
24
|
+
private __fallbackUnlisten?: () => void
|
|
25
|
+
private __throttledUpdate = throttle(() => this.measureAndUpdate(), 80)
|
|
10
26
|
|
|
11
27
|
protected targetId() {
|
|
12
28
|
return `${this.props.graphModel.flowId}:${this.props.model.id}`
|
|
@@ -18,17 +34,25 @@ export class ReactNodeView extends HtmlNode {
|
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
setHtml(rootEl: SVGForeignObjectElement) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
const existed = rootEl.querySelector(
|
|
38
|
+
'.custom-react-node-content',
|
|
39
|
+
) as HTMLElement | null
|
|
40
|
+
if (existed) {
|
|
41
|
+
this.containerEl = existed
|
|
42
|
+
} else {
|
|
43
|
+
const el = document.createElement('div')
|
|
44
|
+
el.className = 'custom-react-node-content'
|
|
45
|
+
this.containerEl = el
|
|
46
|
+
this.renderReactComponent(el)
|
|
47
|
+
rootEl.appendChild(el)
|
|
48
|
+
}
|
|
49
|
+
this.startResizeObserver()
|
|
26
50
|
}
|
|
27
51
|
|
|
28
|
-
confirmUpdate(_rootEl: SVGForeignObjectElement) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
52
|
+
// confirmUpdate(_rootEl: SVGForeignObjectElement) {
|
|
53
|
+
// // TODO: 如有需要,可以先通过继承的方式,自定义该节点的更新逻辑;我们后续会根据实际需求,丰富该功能
|
|
54
|
+
// console.log('_rootEl', _rootEl)
|
|
55
|
+
// }
|
|
32
56
|
|
|
33
57
|
protected renderReactComponent(container: HTMLElement) {
|
|
34
58
|
this.unmountReactComponent()
|
|
@@ -55,6 +79,7 @@ export class ReactNodeView extends HtmlNode {
|
|
|
55
79
|
|
|
56
80
|
protected unmountReactComponent() {
|
|
57
81
|
if (this.rootEl && this.root) {
|
|
82
|
+
this.stopResizeObserver()
|
|
58
83
|
this.root.unmount()
|
|
59
84
|
this.root = undefined
|
|
60
85
|
this.rootEl.innerHTML = ''
|
|
@@ -66,6 +91,74 @@ export class ReactNodeView extends HtmlNode {
|
|
|
66
91
|
this.unmountReactComponent()
|
|
67
92
|
}
|
|
68
93
|
|
|
94
|
+
private measureAndUpdate = () => {
|
|
95
|
+
try {
|
|
96
|
+
const root = this.containerEl as HTMLElement
|
|
97
|
+
if (!root) return
|
|
98
|
+
const target = (root.firstElementChild as HTMLElement) || root
|
|
99
|
+
const rect = target.getBoundingClientRect()
|
|
100
|
+
const width = round(rect.width)
|
|
101
|
+
const height = round(rect.height)
|
|
102
|
+
if (width <= 0 || height <= 0) return
|
|
103
|
+
if (width === this.__lastWidth && height === this.__lastHeight) return
|
|
104
|
+
this.__lastWidth = width
|
|
105
|
+
this.__lastHeight = height
|
|
106
|
+
const props = this.props.model.properties as any
|
|
107
|
+
const extra = get(props, '_showTitle')
|
|
108
|
+
? isNumber(get(props, '_titleHeight'))
|
|
109
|
+
? get(props, '_titleHeight')
|
|
110
|
+
: 28
|
|
111
|
+
: 0
|
|
112
|
+
const baseHeight = clamp(height - extra, 1, Number.MAX_SAFE_INTEGER)
|
|
113
|
+
this.props.model.setProperties({ width, height: baseHeight })
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('measureAndUpdate error', err)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private startResizeObserver() {
|
|
120
|
+
const root = this.containerEl as HTMLElement
|
|
121
|
+
if (!root) return
|
|
122
|
+
try {
|
|
123
|
+
if (isFunction((window as any).ResizeObserver)) {
|
|
124
|
+
this.__resizeObserver = new (window as any).ResizeObserver(
|
|
125
|
+
(entries: any[]) => {
|
|
126
|
+
if (!isArray(entries) || !entries.length) return
|
|
127
|
+
if (this.__resizeRafId) cancelAnimationFrame(this.__resizeRafId)
|
|
128
|
+
this.__resizeRafId = requestAnimationFrame(this.__throttledUpdate)
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
const target = (root.firstElementChild as HTMLElement) || root
|
|
132
|
+
this.__resizeObserver?.observe(target)
|
|
133
|
+
} else {
|
|
134
|
+
window.addEventListener('resize', () => this.__throttledUpdate())
|
|
135
|
+
this.__fallbackUnlisten = () =>
|
|
136
|
+
window.removeEventListener('resize', () => this.__throttledUpdate())
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('startResizeObserver error', err)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private stopResizeObserver() {
|
|
144
|
+
try {
|
|
145
|
+
if (this.__resizeObserver) {
|
|
146
|
+
this.__resizeObserver.disconnect()
|
|
147
|
+
this.__resizeObserver = undefined
|
|
148
|
+
}
|
|
149
|
+
if (this.__resizeRafId) {
|
|
150
|
+
cancelAnimationFrame(this.__resizeRafId)
|
|
151
|
+
this.__resizeRafId = undefined
|
|
152
|
+
}
|
|
153
|
+
if (this.__fallbackUnlisten) {
|
|
154
|
+
this.__fallbackUnlisten()
|
|
155
|
+
this.__fallbackUnlisten = undefined
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('stopResizeObserver error', err)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
69
162
|
// TODO: 确认是否需要重写 onMouseDown 方法
|
|
70
163
|
// handleMouseDown(ev: MouseEvent, x: number, y: number) {
|
|
71
164
|
// const target = ev.target as Element
|
package/src/wrapper.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { PureComponent } from 'react'
|
|
2
2
|
import { BaseNodeModel, EventType, GraphModel } from '@logicflow/core'
|
|
3
3
|
import { reactNodesMap } from './registry'
|
|
4
|
+
import Container from './components/Container'
|
|
4
5
|
|
|
5
6
|
export interface IWrapperProps {
|
|
6
7
|
node: BaseNodeModel
|
|
@@ -49,12 +50,26 @@ export class Wrapper extends PureComponent<IWrapperProps, IWrapperState> {
|
|
|
49
50
|
|
|
50
51
|
if (!content) return null
|
|
51
52
|
|
|
53
|
+
const { _showTitle = false } = node.properties || {}
|
|
54
|
+
|
|
52
55
|
const { component } = content
|
|
53
56
|
if (React.isValidElement(component)) {
|
|
54
|
-
return
|
|
57
|
+
return _showTitle ? (
|
|
58
|
+
<Container node={this.props.node} graph={this.props.graph}>
|
|
59
|
+
{this.clone(component)}
|
|
60
|
+
</Container>
|
|
61
|
+
) : (
|
|
62
|
+
this.clone(component)
|
|
63
|
+
)
|
|
55
64
|
}
|
|
56
65
|
const FC = component as React.FC
|
|
57
|
-
return
|
|
66
|
+
return _showTitle ? (
|
|
67
|
+
<Container node={this.props.node} graph={this.props.graph}>
|
|
68
|
+
{this.clone(<FC />)}
|
|
69
|
+
</Container>
|
|
70
|
+
) : (
|
|
71
|
+
this.clone(<FC />)
|
|
72
|
+
)
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
|