@tldraw/editor 3.14.0-canary.2f3caa391d5d → 3.14.0-canary.306affbc4326
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/dist-cjs/index.d.ts +27 -34
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +1 -0
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +72 -42
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
- package/dist-cjs/lib/utils/richText.js +7 -2
- package/dist-cjs/lib/utils/richText.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +27 -34
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +1 -0
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +72 -42
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/utils/richText.mjs +8 -3
- package/dist-esm/lib/utils/richText.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +458 -523
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/lib/editor/Editor.ts +2 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +116 -86
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
- package/src/lib/utils/richText.ts +9 -3
- package/src/version.ts +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "A tiny little drawing app (editor).",
|
|
4
|
-
"version": "3.14.0-canary.
|
|
4
|
+
"version": "3.14.0-canary.306affbc4326",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"@tiptap/core": "^2.9.1",
|
|
49
49
|
"@tiptap/pm": "^2.9.1",
|
|
50
50
|
"@tiptap/react": "^2.9.1",
|
|
51
|
-
"@tldraw/state": "3.14.0-canary.
|
|
52
|
-
"@tldraw/state-react": "3.14.0-canary.
|
|
53
|
-
"@tldraw/store": "3.14.0-canary.
|
|
54
|
-
"@tldraw/tlschema": "3.14.0-canary.
|
|
55
|
-
"@tldraw/utils": "3.14.0-canary.
|
|
56
|
-
"@tldraw/validate": "3.14.0-canary.
|
|
51
|
+
"@tldraw/state": "3.14.0-canary.306affbc4326",
|
|
52
|
+
"@tldraw/state-react": "3.14.0-canary.306affbc4326",
|
|
53
|
+
"@tldraw/store": "3.14.0-canary.306affbc4326",
|
|
54
|
+
"@tldraw/tlschema": "3.14.0-canary.306affbc4326",
|
|
55
|
+
"@tldraw/utils": "3.14.0-canary.306affbc4326",
|
|
56
|
+
"@tldraw/validate": "3.14.0-canary.306affbc4326",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
package/src/index.ts
CHANGED
|
@@ -174,6 +174,7 @@ export {
|
|
|
174
174
|
} from './lib/editor/managers/SnapManager/SnapManager'
|
|
175
175
|
export {
|
|
176
176
|
TextManager,
|
|
177
|
+
type TLMeasureTextOpts,
|
|
177
178
|
type TLMeasureTextSpanOpts,
|
|
178
179
|
} from './lib/editor/managers/TextManager/TextManager'
|
|
179
180
|
export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -348,6 +348,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
348
348
|
this.getContainer = getContainer
|
|
349
349
|
|
|
350
350
|
this.textMeasure = new TextManager(this)
|
|
351
|
+
this.disposables.add(() => this.textMeasure.dispose())
|
|
352
|
+
|
|
351
353
|
this.fonts = new FontManager(this, fontAssetUrls)
|
|
352
354
|
|
|
353
355
|
this._tickManager = new TickManager(this)
|
|
@@ -96,8 +96,7 @@ export class FontManager {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
areResultsEqual: areArraysShallowEqual,
|
|
99
|
-
|
|
100
|
-
areRecordsEqual: (a, b) => a.props.richText === b.props.richText,
|
|
99
|
+
areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
|
|
101
100
|
}
|
|
102
101
|
)
|
|
103
102
|
|
|
@@ -99,7 +99,7 @@ describe('TextManager', () => {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
it('should handle empty text', () => {
|
|
102
|
-
const result = textManager.measureText('', defaultOpts)
|
|
102
|
+
const result = textManager.measureText('', { ...defaultOpts, measureScrollWidth: true })
|
|
103
103
|
expect(result).toHaveProperty('x', 0)
|
|
104
104
|
expect(result).toHaveProperty('y', 0)
|
|
105
105
|
expect(result).toHaveProperty('w')
|
|
@@ -128,7 +128,6 @@ describe('TextManager', () => {
|
|
|
128
128
|
y: 0,
|
|
129
129
|
w: expect.any(Number),
|
|
130
130
|
h: expect.any(Number),
|
|
131
|
-
scrollWidth: expect.any(Number),
|
|
132
131
|
})
|
|
133
132
|
})
|
|
134
133
|
|
|
@@ -141,7 +140,6 @@ describe('TextManager', () => {
|
|
|
141
140
|
y: 0,
|
|
142
141
|
w: expect.any(Number),
|
|
143
142
|
h: expect.any(Number),
|
|
144
|
-
scrollWidth: expect.any(Number),
|
|
145
143
|
})
|
|
146
144
|
})
|
|
147
145
|
|
|
@@ -154,7 +152,6 @@ describe('TextManager', () => {
|
|
|
154
152
|
y: 0,
|
|
155
153
|
w: expect.any(Number),
|
|
156
154
|
h: expect.any(Number),
|
|
157
|
-
scrollWidth: expect.any(Number),
|
|
158
155
|
})
|
|
159
156
|
})
|
|
160
157
|
|
|
@@ -173,7 +170,6 @@ describe('TextManager', () => {
|
|
|
173
170
|
y: 0,
|
|
174
171
|
w: expect.any(Number),
|
|
175
172
|
h: expect.any(Number),
|
|
176
|
-
scrollWidth: expect.any(Number),
|
|
177
173
|
})
|
|
178
174
|
})
|
|
179
175
|
})
|
|
@@ -20,6 +20,27 @@ const textAlignmentsForLtr = {
|
|
|
20
20
|
'end-legacy': 'right',
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** @public */
|
|
24
|
+
export interface TLMeasureTextOpts {
|
|
25
|
+
fontStyle: string
|
|
26
|
+
fontWeight: string
|
|
27
|
+
fontFamily: string
|
|
28
|
+
fontSize: number
|
|
29
|
+
lineHeight: number
|
|
30
|
+
/**
|
|
31
|
+
* When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
|
|
32
|
+
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
33
|
+
* space are preserved.
|
|
34
|
+
*/
|
|
35
|
+
maxWidth: null | number
|
|
36
|
+
minWidth?: null | number
|
|
37
|
+
// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts
|
|
38
|
+
padding: string
|
|
39
|
+
otherStyles?: Record<string, string>
|
|
40
|
+
disableOverflowWrapBreaking?: boolean
|
|
41
|
+
measureScrollWidth?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/** @public */
|
|
24
45
|
export interface TLMeasureTextSpanOpts {
|
|
25
46
|
overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
|
|
@@ -33,96 +54,98 @@ export interface TLMeasureTextSpanOpts {
|
|
|
33
54
|
lineHeight: number
|
|
34
55
|
textAlign: TLDefaultHorizontalAlignStyle
|
|
35
56
|
otherStyles?: Record<string, string>
|
|
57
|
+
measureScrollWidth?: boolean
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
const spaceCharacterRegex = /\s/
|
|
39
61
|
|
|
40
62
|
/** @public */
|
|
41
63
|
export class TextManager {
|
|
42
|
-
private
|
|
64
|
+
private elm: HTMLDivElement
|
|
65
|
+
private defaultStyles: Record<string, string | null>
|
|
43
66
|
|
|
44
67
|
constructor(public editor: Editor) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
const elm = document.createElement('div')
|
|
69
|
+
elm.classList.add('tl-text')
|
|
70
|
+
elm.classList.add('tl-text-measure')
|
|
71
|
+
elm.setAttribute('dir', 'auto')
|
|
72
|
+
elm.tabIndex = -1
|
|
73
|
+
this.editor.getContainer().appendChild(elm)
|
|
74
|
+
|
|
75
|
+
// we need to save the default styles so that we can restore them when we're done
|
|
76
|
+
// these must be the css names, not the js names for the styles
|
|
77
|
+
this.defaultStyles = {
|
|
78
|
+
'word-break': 'auto',
|
|
79
|
+
width: null,
|
|
80
|
+
height: null,
|
|
81
|
+
'max-width': null,
|
|
82
|
+
'min-width': null,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.elm = elm
|
|
49
86
|
}
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
|
|
61
|
-
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
62
|
-
* space are preserved.
|
|
63
|
-
*/
|
|
64
|
-
maxWidth: null | number
|
|
65
|
-
minWidth?: null | number
|
|
66
|
-
padding: string
|
|
67
|
-
disableOverflowWrapBreaking?: boolean
|
|
88
|
+
dispose() {
|
|
89
|
+
return this.elm.remove()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private resetElmStyles() {
|
|
93
|
+
const { elm, defaultStyles } = this
|
|
94
|
+
for (const key in defaultStyles) {
|
|
95
|
+
elm.style.setProperty(key, defaultStyles[key])
|
|
68
96
|
}
|
|
69
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
70
100
|
const div = document.createElement('div')
|
|
71
101
|
div.textContent = normalizeTextForDom(textToMeasure)
|
|
72
102
|
return this.measureHtml(div.innerHTML, opts)
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
measureHtml(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
86
|
-
* space are preserved.
|
|
87
|
-
*/
|
|
88
|
-
maxWidth: null | number
|
|
89
|
-
minWidth?: null | number
|
|
90
|
-
otherStyles?: Record<string, string>
|
|
91
|
-
padding: string
|
|
92
|
-
disableOverflowWrapBreaking?: boolean
|
|
105
|
+
measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
106
|
+
const { elm } = this
|
|
107
|
+
|
|
108
|
+
if (opts.otherStyles) {
|
|
109
|
+
for (const key in opts.otherStyles) {
|
|
110
|
+
if (!this.defaultStyles[key]) {
|
|
111
|
+
// we need to save the original style so that we can restore it when we're done
|
|
112
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
93
115
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
elm.innerHTML = html
|
|
118
|
+
|
|
119
|
+
// Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
|
|
120
|
+
this.resetElmStyles()
|
|
121
|
+
|
|
122
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
123
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
124
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
125
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
126
|
+
elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
|
|
127
|
+
elm.style.setProperty('padding', opts.padding)
|
|
128
|
+
|
|
129
|
+
if (opts.maxWidth) {
|
|
130
|
+
elm.style.setProperty('max-width', opts.maxWidth + 'px')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts.minWidth) {
|
|
134
|
+
elm.style.setProperty('min-width', opts.minWidth + 'px')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (opts.disableOverflowWrapBreaking) {
|
|
138
|
+
elm.style.setProperty('overflow-wrap', 'normal')
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
if (opts.otherStyles) {
|
|
118
142
|
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
119
|
-
|
|
143
|
+
elm.style.setProperty(key, value)
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
const scrollWidth =
|
|
124
|
-
const rect =
|
|
125
|
-
wrapperElm.remove()
|
|
147
|
+
const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
|
|
148
|
+
const rect = elm.getBoundingClientRect()
|
|
126
149
|
|
|
127
150
|
return {
|
|
128
151
|
x: 0,
|
|
@@ -247,27 +270,29 @@ export class TextManager {
|
|
|
247
270
|
): { text: string; box: BoxModel }[] {
|
|
248
271
|
if (textToMeasure === '') return []
|
|
249
272
|
|
|
250
|
-
const elm = this
|
|
251
|
-
|
|
273
|
+
const { elm } = this
|
|
274
|
+
|
|
275
|
+
if (opts.otherStyles) {
|
|
276
|
+
for (const key in opts.otherStyles) {
|
|
277
|
+
if (!this.defaultStyles[key]) {
|
|
278
|
+
// we need to save the original style so that we can restore it when we're done
|
|
279
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.resetElmStyles()
|
|
285
|
+
|
|
286
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
287
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
288
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
289
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
290
|
+
elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
|
|
252
291
|
|
|
253
292
|
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
|
254
|
-
elm.setAttribute('dir', 'auto')
|
|
255
|
-
// N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
|
|
256
|
-
// is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.
|
|
257
|
-
elm.style.setProperty('unicode-bidi', 'plaintext')
|
|
258
293
|
elm.style.setProperty('width', `${elementWidth}px`)
|
|
259
294
|
elm.style.setProperty('height', 'min-content')
|
|
260
|
-
elm.style.setProperty('font-size', `${opts.fontSize}px`)
|
|
261
|
-
elm.style.setProperty('font-family', opts.fontFamily)
|
|
262
|
-
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
263
|
-
elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
|
|
264
295
|
elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
|
|
265
|
-
elm.style.setProperty('font-style', opts.fontStyle)
|
|
266
|
-
if (opts.otherStyles) {
|
|
267
|
-
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
268
|
-
elm.style.setProperty(key, value)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
296
|
|
|
272
297
|
const shouldTruncateToFirstLine =
|
|
273
298
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
|
@@ -277,6 +302,12 @@ export class TextManager {
|
|
|
277
302
|
elm.style.setProperty('word-break', 'break-all')
|
|
278
303
|
}
|
|
279
304
|
|
|
305
|
+
if (opts.otherStyles) {
|
|
306
|
+
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
307
|
+
elm.style.setProperty(key, value)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
const normalizedText = normalizeTextForDom(textToMeasure)
|
|
281
312
|
|
|
282
313
|
// Render the text into the measurement element:
|
|
@@ -313,11 +344,10 @@ export class TextManager {
|
|
|
313
344
|
h: lastSpan.box.h,
|
|
314
345
|
},
|
|
315
346
|
})
|
|
347
|
+
|
|
316
348
|
return truncatedSpans
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
elm.remove()
|
|
320
|
-
|
|
321
351
|
return spans
|
|
322
352
|
}
|
|
323
353
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'
|
|
2
|
-
import { Node } from '@tiptap/pm/model'
|
|
2
|
+
import { Node, Schema } from '@tiptap/pm/model'
|
|
3
3
|
import { EditorProviderProps } from '@tiptap/react'
|
|
4
4
|
import { TLRichText } from '@tldraw/tlschema'
|
|
5
|
-
import { assert } from '@tldraw/utils'
|
|
5
|
+
import { assert, WeakCache } from '@tldraw/utils'
|
|
6
6
|
import { Editor } from '../editor/Editor'
|
|
7
7
|
import { TLFontFace } from '../editor/managers/FontManager/FontManager'
|
|
8
8
|
|
|
@@ -39,6 +39,11 @@ export type RichTextFontVisitor = (
|
|
|
39
39
|
addFont: (font: TLFontFace) => void
|
|
40
40
|
) => RichTextFontVisitorState
|
|
41
41
|
|
|
42
|
+
const schemaCache = new WeakCache<EditorProviderProps, Schema>()
|
|
43
|
+
export function getTipTapSchema(tipTapConfig: EditorProviderProps) {
|
|
44
|
+
return schemaCache.get(tipTapConfig, () => getSchema(tipTapConfig.extensions ?? []))
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
/** @public */
|
|
43
48
|
export function getFontsFromRichText(
|
|
44
49
|
editor: Editor,
|
|
@@ -49,7 +54,8 @@ export function getFontsFromRichText(
|
|
|
49
54
|
assert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')
|
|
50
55
|
assert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')
|
|
51
56
|
|
|
52
|
-
const schema =
|
|
57
|
+
const schema = getTipTapSchema(tipTapConfig)
|
|
58
|
+
|
|
53
59
|
const rootNode = Node.fromJSON(schema, richText as JSONContent)
|
|
54
60
|
|
|
55
61
|
const fonts = new Set<TLFontFace>()
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '3.14.0-canary.
|
|
4
|
+
export const version = '3.14.0-canary.306affbc4326'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2025-06-
|
|
8
|
-
patch: '2025-06-
|
|
7
|
+
minor: '2025-06-16T13:31:38.369Z',
|
|
8
|
+
patch: '2025-06-16T13:31:38.369Z',
|
|
9
9
|
}
|