@radio-garden/ditojs-admin 2.85.2-0.5067ad799
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/README.md +180 -0
- package/dist/dito-admin.css +1 -0
- package/dist/dito-admin.es.js +12106 -0
- package/dist/dito-admin.umd.js +7 -0
- package/package.json +96 -0
- package/src/DitoAdmin.js +293 -0
- package/src/DitoComponent.js +34 -0
- package/src/DitoContext.js +318 -0
- package/src/DitoTypeComponent.js +42 -0
- package/src/DitoUser.js +12 -0
- package/src/appState.js +12 -0
- package/src/components/DitoAccount.vue +60 -0
- package/src/components/DitoAffix.vue +68 -0
- package/src/components/DitoAffixes.vue +200 -0
- package/src/components/DitoButtons.vue +80 -0
- package/src/components/DitoClipboard.vue +186 -0
- package/src/components/DitoContainer.vue +374 -0
- package/src/components/DitoCreateButton.vue +146 -0
- package/src/components/DitoDialog.vue +242 -0
- package/src/components/DitoDraggable.vue +117 -0
- package/src/components/DitoEditButtons.vue +135 -0
- package/src/components/DitoErrors.vue +83 -0
- package/src/components/DitoForm.vue +521 -0
- package/src/components/DitoFormInner.vue +26 -0
- package/src/components/DitoFormNested.vue +17 -0
- package/src/components/DitoHeader.vue +84 -0
- package/src/components/DitoLabel.vue +200 -0
- package/src/components/DitoMenu.vue +186 -0
- package/src/components/DitoNavigation.vue +40 -0
- package/src/components/DitoNotifications.vue +170 -0
- package/src/components/DitoPagination.vue +42 -0
- package/src/components/DitoPane.vue +334 -0
- package/src/components/DitoPanel.vue +256 -0
- package/src/components/DitoPanels.vue +61 -0
- package/src/components/DitoRoot.vue +524 -0
- package/src/components/DitoSchema.vue +846 -0
- package/src/components/DitoSchemaInlined.vue +97 -0
- package/src/components/DitoScopes.vue +76 -0
- package/src/components/DitoSidebar.vue +50 -0
- package/src/components/DitoSpinner.vue +95 -0
- package/src/components/DitoTableCell.vue +64 -0
- package/src/components/DitoTableHead.vue +121 -0
- package/src/components/DitoTabs.vue +103 -0
- package/src/components/DitoTrail.vue +124 -0
- package/src/components/DitoTreeItem.vue +420 -0
- package/src/components/DitoUploadFile.vue +199 -0
- package/src/components/DitoVNode.vue +14 -0
- package/src/components/DitoView.vue +143 -0
- package/src/components/index.js +42 -0
- package/src/directives/resize.js +83 -0
- package/src/index.js +1 -0
- package/src/mixins/ContextMixin.js +68 -0
- package/src/mixins/DataMixin.js +131 -0
- package/src/mixins/DitoMixin.js +591 -0
- package/src/mixins/DomMixin.js +29 -0
- package/src/mixins/EmitterMixin.js +158 -0
- package/src/mixins/ItemMixin.js +144 -0
- package/src/mixins/LoadingMixin.js +23 -0
- package/src/mixins/NumberMixin.js +118 -0
- package/src/mixins/OptionsMixin.js +304 -0
- package/src/mixins/PulldownMixin.js +63 -0
- package/src/mixins/ResourceMixin.js +398 -0
- package/src/mixins/RouteMixin.js +190 -0
- package/src/mixins/SchemaParentMixin.js +33 -0
- package/src/mixins/SortableMixin.js +49 -0
- package/src/mixins/SourceMixin.js +734 -0
- package/src/mixins/TextMixin.js +26 -0
- package/src/mixins/TypeMixin.js +280 -0
- package/src/mixins/ValidationMixin.js +119 -0
- package/src/mixins/ValidatorMixin.js +57 -0
- package/src/mixins/ValueMixin.js +31 -0
- package/src/styles/_base.scss +17 -0
- package/src/styles/_button.scss +191 -0
- package/src/styles/_imports.scss +3 -0
- package/src/styles/_info.scss +19 -0
- package/src/styles/_layout.scss +19 -0
- package/src/styles/_pulldown.scss +38 -0
- package/src/styles/_scroll.scss +13 -0
- package/src/styles/_settings.scss +88 -0
- package/src/styles/_table.scss +223 -0
- package/src/styles/_tippy.scss +45 -0
- package/src/styles/style.scss +9 -0
- package/src/types/DitoTypeButton.vue +143 -0
- package/src/types/DitoTypeCheckbox.vue +27 -0
- package/src/types/DitoTypeCheckboxes.vue +65 -0
- package/src/types/DitoTypeCode.vue +199 -0
- package/src/types/DitoTypeColor.vue +272 -0
- package/src/types/DitoTypeComponent.vue +31 -0
- package/src/types/DitoTypeComputed.vue +50 -0
- package/src/types/DitoTypeDate.vue +99 -0
- package/src/types/DitoTypeLabel.vue +23 -0
- package/src/types/DitoTypeList.vue +364 -0
- package/src/types/DitoTypeMarkup.vue +700 -0
- package/src/types/DitoTypeMultiselect.vue +522 -0
- package/src/types/DitoTypeNumber.vue +66 -0
- package/src/types/DitoTypeObject.vue +136 -0
- package/src/types/DitoTypePanel.vue +18 -0
- package/src/types/DitoTypeProgress.vue +40 -0
- package/src/types/DitoTypeRadio.vue +45 -0
- package/src/types/DitoTypeSection.vue +80 -0
- package/src/types/DitoTypeSelect.vue +133 -0
- package/src/types/DitoTypeSlider.vue +66 -0
- package/src/types/DitoTypeSpacer.vue +11 -0
- package/src/types/DitoTypeSwitch.vue +40 -0
- package/src/types/DitoTypeText.vue +101 -0
- package/src/types/DitoTypeTextarea.vue +48 -0
- package/src/types/DitoTypeTreeList.vue +193 -0
- package/src/types/DitoTypeUpload.vue +503 -0
- package/src/types/index.js +30 -0
- package/src/utils/SchemaGraph.js +147 -0
- package/src/utils/accessor.js +75 -0
- package/src/utils/agent.js +47 -0
- package/src/utils/data.js +92 -0
- package/src/utils/filter.js +266 -0
- package/src/utils/math.js +14 -0
- package/src/utils/options.js +48 -0
- package/src/utils/path.js +5 -0
- package/src/utils/resource.js +44 -0
- package/src/utils/route.js +53 -0
- package/src/utils/schema.js +1121 -0
- package/src/utils/type.js +81 -0
- package/src/utils/uid.js +15 -0
- package/src/utils/units.js +5 -0
- package/src/validators/_creditcard.js +6 -0
- package/src/validators/_decimals.js +11 -0
- package/src/validators/_domain.js +6 -0
- package/src/validators/_email.js +6 -0
- package/src/validators/_hostname.js +6 -0
- package/src/validators/_integer.js +6 -0
- package/src/validators/_max.js +6 -0
- package/src/validators/_min.js +6 -0
- package/src/validators/_password.js +5 -0
- package/src/validators/_range.js +6 -0
- package/src/validators/_required.js +9 -0
- package/src/validators/_url.js +6 -0
- package/src/validators/index.js +12 -0
- package/src/verbs.js +17 -0
- package/types/index.d.ts +3298 -0
- package/types/tests/admin.test-d.ts +27 -0
- package/types/tests/component-buttons.test-d.ts +44 -0
- package/types/tests/component-list.test-d.ts +159 -0
- package/types/tests/component-misc.test-d.ts +137 -0
- package/types/tests/component-object.test-d.ts +69 -0
- package/types/tests/component-section.test-d.ts +174 -0
- package/types/tests/component-select.test-d.ts +107 -0
- package/types/tests/components.test-d.ts +81 -0
- package/types/tests/context.test-d.ts +31 -0
- package/types/tests/fixtures.ts +24 -0
- package/types/tests/form.test-d.ts +109 -0
- package/types/tests/instance.test-d.ts +20 -0
- package/types/tests/schema-features.test-d.ts +402 -0
- package/types/tests/variance.test-d.ts +125 -0
- package/types/tests/view.test-d.ts +146 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import DitoContext from '../DitoContext.js'
|
|
2
|
+
import DataMixin from './DataMixin.js'
|
|
3
|
+
import {
|
|
4
|
+
hasViewSchema,
|
|
5
|
+
getViewEditPath,
|
|
6
|
+
getMultipleValue
|
|
7
|
+
} from '../utils/schema.js'
|
|
8
|
+
import { getSchemaAccessor } from '../utils/accessor.js'
|
|
9
|
+
import { setTemporaryId, isReference } from '../utils/data.js'
|
|
10
|
+
import {
|
|
11
|
+
isObject,
|
|
12
|
+
isArray,
|
|
13
|
+
isString,
|
|
14
|
+
isFunction,
|
|
15
|
+
normalizeDataPath,
|
|
16
|
+
labelize,
|
|
17
|
+
debounceAsync
|
|
18
|
+
} from '@ditojs/utils'
|
|
19
|
+
|
|
20
|
+
// @vue/component
|
|
21
|
+
export default {
|
|
22
|
+
mixins: [DataMixin],
|
|
23
|
+
|
|
24
|
+
computed: {
|
|
25
|
+
// @overridable
|
|
26
|
+
multiple() {
|
|
27
|
+
return getMultipleValue(this.schema)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
selectedValue: {
|
|
31
|
+
get() {
|
|
32
|
+
const convertValue = value => {
|
|
33
|
+
const val = this.relate
|
|
34
|
+
? this.getValueForOption(value)
|
|
35
|
+
: value
|
|
36
|
+
|
|
37
|
+
return this.hasOptions
|
|
38
|
+
? this.getOptionForValue(val)
|
|
39
|
+
? val
|
|
40
|
+
: null
|
|
41
|
+
: value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const value =
|
|
45
|
+
this.multiple && isArray(this.value)
|
|
46
|
+
? this.value.map(convertValue).filter(value => value !== null)
|
|
47
|
+
: convertValue(this.value)
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
// As soon as the options are available, and...
|
|
51
|
+
this.hasOptions && (
|
|
52
|
+
// ...if the value is forced to null because a disappeared option...
|
|
53
|
+
value === null && this.value !== null ||
|
|
54
|
+
// ...or if the value is a reference, replace it with its option
|
|
55
|
+
// value, so that it'll hold actual data, not just a reference id.
|
|
56
|
+
isReference(this.value)
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
// TODO: Fix side-effects
|
|
60
|
+
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
61
|
+
this.selectedValue = value
|
|
62
|
+
}
|
|
63
|
+
return value
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
set(value) {
|
|
67
|
+
const convertValue = value =>
|
|
68
|
+
this.relate
|
|
69
|
+
? this.getOptionForValue(value)
|
|
70
|
+
: value
|
|
71
|
+
|
|
72
|
+
this.value =
|
|
73
|
+
this.multiple && isArray(value)
|
|
74
|
+
? value.map(convertValue)
|
|
75
|
+
: convertValue(value)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
selectedOption() {
|
|
80
|
+
return this.getOptionForValue(this.selectedValue)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
options() {
|
|
84
|
+
const data = this.handleDataSchema(this.schema.options, 'options') ?? []
|
|
85
|
+
if (!isArray(data)) {
|
|
86
|
+
throw new Error(`Invalid options data, should be array: ${data}`)
|
|
87
|
+
}
|
|
88
|
+
return this.processOptions(data)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
activeOptions() {
|
|
92
|
+
// This is overridden in `TypeMultiselect` to return the `searchedOptions`
|
|
93
|
+
// when a search filter was applied.
|
|
94
|
+
return this.options
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
hasOptions() {
|
|
98
|
+
return this.activeOptions.length > 0
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
relate: getSchemaAccessor('relate', {
|
|
102
|
+
// TODO: Convert to `relateBy: 'id'`
|
|
103
|
+
type: Boolean,
|
|
104
|
+
default: false,
|
|
105
|
+
// We cannot use schema accessor callback magic for `relate` as we need
|
|
106
|
+
// this outside of the component's life-span, see `processData()` below.
|
|
107
|
+
callback: false
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
groupBy: getSchemaAccessor('groupBy', {
|
|
111
|
+
type: String,
|
|
112
|
+
default: null
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
// TODO: Rename to `options.labelKey` / `optionLabelKey`?
|
|
116
|
+
optionLabel: getSchemaAccessor('options.label', {
|
|
117
|
+
type: [String, Function],
|
|
118
|
+
default: null,
|
|
119
|
+
get(label) {
|
|
120
|
+
// If no `label` was provided but the options are objects, assume a
|
|
121
|
+
// default value of 'label':
|
|
122
|
+
return (
|
|
123
|
+
label ||
|
|
124
|
+
this.getOptionKey('label') ||
|
|
125
|
+
null
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
// TODO: Rename to `options.valueKey` / `optionValueKey`?
|
|
131
|
+
optionValue: getSchemaAccessor('options.value', {
|
|
132
|
+
type: [String, Function],
|
|
133
|
+
default: null,
|
|
134
|
+
get(value) {
|
|
135
|
+
// If no `label` was provided but the options are objects, assume a
|
|
136
|
+
// default value of 'value':
|
|
137
|
+
return (
|
|
138
|
+
value ||
|
|
139
|
+
this.relate && 'id' ||
|
|
140
|
+
this.getOptionKey('value') ||
|
|
141
|
+
null
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
optionEquals: getSchemaAccessor('options.equals', {
|
|
147
|
+
type: Function,
|
|
148
|
+
default: null
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
// TODO: Consider moving search to `options.search`?
|
|
152
|
+
searchFilter: getSchemaAccessor('search', {
|
|
153
|
+
type: [Object, Function],
|
|
154
|
+
default: null,
|
|
155
|
+
get(search) {
|
|
156
|
+
if (search) {
|
|
157
|
+
const { filter, debounce } = isFunction(search)
|
|
158
|
+
? { filter: search }
|
|
159
|
+
: search
|
|
160
|
+
return debounce ? debounceAsync(filter, debounce) : filter
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}),
|
|
164
|
+
|
|
165
|
+
editable: getSchemaAccessor('editable', {
|
|
166
|
+
type: Boolean,
|
|
167
|
+
default: false,
|
|
168
|
+
get(editable) {
|
|
169
|
+
return (
|
|
170
|
+
editable &&
|
|
171
|
+
hasViewSchema(this.schema, this.context)
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
editPath() {
|
|
177
|
+
return this.editable && this.selectedValue
|
|
178
|
+
? getViewEditPath(this.schema, this.selectedValue, this.context)
|
|
179
|
+
: null
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
groupByLabel() {
|
|
183
|
+
return this.groupBy ? 'label' : null
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
groupByOptions() {
|
|
187
|
+
return this.groupBy ? 'options' : null
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
methods: {
|
|
192
|
+
getOptionKey(key) {
|
|
193
|
+
const [option] = this.activeOptions
|
|
194
|
+
return isObject(option) && key in option ? key : null
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
processOptions(options) {
|
|
198
|
+
if (options.length) {
|
|
199
|
+
if (this.relate) {
|
|
200
|
+
// If ids are missing and we want to relate, set temporary ids.
|
|
201
|
+
// NOTE: We need to modify the actual data, making a copy won't work
|
|
202
|
+
// as it won't propagate.
|
|
203
|
+
// NOTE: This only makes sense if the data is from the graph that
|
|
204
|
+
// we're currently editing.
|
|
205
|
+
for (const option of options) {
|
|
206
|
+
if (!('id' in option)) {
|
|
207
|
+
// TODO: Fix side-effects
|
|
208
|
+
setTemporaryId(option, 'id')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (this.groupBy) {
|
|
213
|
+
const grouped = {}
|
|
214
|
+
options = options.reduce(
|
|
215
|
+
(results, option) => {
|
|
216
|
+
const group = option[this.groupBy]
|
|
217
|
+
let entry = grouped[group]
|
|
218
|
+
if (!entry) {
|
|
219
|
+
entry = grouped[group] = {
|
|
220
|
+
[this.groupByLabel]: group,
|
|
221
|
+
[this.groupByOptions]: []
|
|
222
|
+
}
|
|
223
|
+
results.push(entry)
|
|
224
|
+
}
|
|
225
|
+
entry[this.groupByOptions].push(option)
|
|
226
|
+
return results
|
|
227
|
+
},
|
|
228
|
+
[]
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return options
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
getOptionForValue(value) {
|
|
236
|
+
const findOption = (options, value, groupBy) => {
|
|
237
|
+
// Search for the option object with the given value and return the
|
|
238
|
+
// whole object.
|
|
239
|
+
for (const option of options) {
|
|
240
|
+
if (groupBy) {
|
|
241
|
+
const found = findOption(option.options, value, null)
|
|
242
|
+
if (found) {
|
|
243
|
+
return found
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
const matches = this.optionEquals
|
|
247
|
+
? this.optionEquals(new DitoContext(this, { value, option }))
|
|
248
|
+
: value === this.getValueForOption(option)
|
|
249
|
+
if (matches) {
|
|
250
|
+
return option
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return this.optionValue
|
|
257
|
+
? findOption(this.activeOptions, value, this.groupBy)
|
|
258
|
+
: value
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
getValueForOption(option) {
|
|
262
|
+
const { optionValue } = this
|
|
263
|
+
return isString(optionValue)
|
|
264
|
+
? option?.[optionValue] ?? null
|
|
265
|
+
: isFunction(optionValue)
|
|
266
|
+
? optionValue(new DitoContext(this, { option }))
|
|
267
|
+
: option
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
getLabelForOption(option) {
|
|
271
|
+
const { optionLabel } = this
|
|
272
|
+
return isString(optionLabel)
|
|
273
|
+
? option?.[optionLabel]
|
|
274
|
+
: isFunction(optionLabel)
|
|
275
|
+
? optionLabel(new DitoContext(this, { option }))
|
|
276
|
+
: labelize(`${option}`)
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
processValue({ schema, value, dataPath }, graph) {
|
|
281
|
+
if (schema.relate) {
|
|
282
|
+
// For internally relating data (`schema.options.dataPath`), we need to
|
|
283
|
+
// process both the options (for '#ref') and the value ('#id').
|
|
284
|
+
// See `DataMixin.handleDataSchema()`:
|
|
285
|
+
const path = schema.options?.dataPath
|
|
286
|
+
const relatedDataPath = path
|
|
287
|
+
? normalizeDataPath(`${dataPath}/${path}`)
|
|
288
|
+
: null
|
|
289
|
+
graph.addRelation(dataPath, relatedDataPath, schema)
|
|
290
|
+
if (relatedDataPath) {
|
|
291
|
+
graph.setSourceRelated(relatedDataPath)
|
|
292
|
+
}
|
|
293
|
+
// Convert relating objects to a shallow copy with only the id left.
|
|
294
|
+
// TODO: Convert to using `relateBy`:
|
|
295
|
+
const processRelate = value => (value ? { id: value.id } : value)
|
|
296
|
+
// Selected options can be both objects & arrays, e.g. 'checkboxes':
|
|
297
|
+
value =
|
|
298
|
+
getMultipleValue(schema) && isArray(value)
|
|
299
|
+
? value.map(processRelate)
|
|
300
|
+
: processRelate(value)
|
|
301
|
+
}
|
|
302
|
+
return value
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import DomMixin from './DomMixin.js'
|
|
2
|
+
|
|
3
|
+
// @vue/component
|
|
4
|
+
export default {
|
|
5
|
+
mixins: [DomMixin],
|
|
6
|
+
|
|
7
|
+
data() {
|
|
8
|
+
return {
|
|
9
|
+
pulldown: {
|
|
10
|
+
open: false,
|
|
11
|
+
startTime: 0,
|
|
12
|
+
checkTime: true,
|
|
13
|
+
events: {
|
|
14
|
+
mousedown: () => {
|
|
15
|
+
this.setPulldownOpen(false)
|
|
16
|
+
this.pulldown.handlers.remove()
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
mouseup: () => {
|
|
20
|
+
if (this.onPulldownMouseUp()) {
|
|
21
|
+
this.pulldown.handlers.remove()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handlers: null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
methods: {
|
|
31
|
+
onPulldownMouseDown(value = null) {
|
|
32
|
+
if (value === null) {
|
|
33
|
+
this.setPulldownOpen(true)
|
|
34
|
+
this.checkTime = true
|
|
35
|
+
} else {
|
|
36
|
+
this.checkTime = false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
onPulldownMouseUp(value = null) {
|
|
41
|
+
const { startTime } = this.pulldown
|
|
42
|
+
if (!this.checkTime || startTime && (Date.now() - startTime > 250)) {
|
|
43
|
+
this.setPulldownOpen(false)
|
|
44
|
+
if (value !== null) {
|
|
45
|
+
this.onPulldownSelect(value)
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
onPulldownSelect(/* value */) {
|
|
52
|
+
// NOTE: To be overridden.
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
setPulldownOpen(open) {
|
|
56
|
+
this.pulldown.open = open
|
|
57
|
+
this.pulldown.startTime = open ? Date.now() : 0
|
|
58
|
+
if (open) {
|
|
59
|
+
this.pulldown.handlers = this.domOn(document, this.pulldown.events)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|