forked from sproutcore/sproutcore
/
list_item.js
764 lines (624 loc) · 24 KB
/
list_item.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
// portions copyright @2009 Apple Inc.
// License: Licened under MIT license (see license.js)
// ==========================================================================
SC.LIST_ITEM_ACTION_CANCEL = 'sc-list-item-cancel-action';
SC.LIST_ITEM_ACTION_REFRESH = 'sc-list-item-cancel-refresh';
SC.LIST_ITEM_ACTION_EJECT = 'sc-list-item-cancel-eject';
/**
@class
Many times list items need to display a lot more than just a label of text.
You often need to include checkboxes, icons, right icons, extra counts and
an action or warning icon to the far right.
A ListItemView can implement all of this for you in a more efficient way
than you might get if you simply put together a list item on your own using
views.
@extends SC.View
@extends SC.Control
@extends SC.Editable
@extends SC.StaticLayout
@since SproutCore 1.0
*/
SC.ListItemView = SC.View.extend(
SC.StaticLayout,
SC.Control,
/** @scope SC.ListItemView.prototype */ {
classNames: ['sc-list-item-view'],
// ..........................................................
// KEY PROPERTIES
//
/**
The content object the list item will display.
@type SC.Object
*/
content: null,
/**
(displayDelegate) True if you want the item view to display an icon.
If false, the icon on the list item view will be hidden. Otherwise,
space will be left for the icon next to the list item view.
*/
hasContentIcon: NO,
/**
(displayDelegate) True if you want the item view to display a right icon.
If false, the icon on the list item view will be hidden. Otherwise,
space will be left for the icon next to the list item view.
*/
hasContentRightIcon: NO,
/**
(displayDelegate) True if you want space to be allocated for a branch
arrow.
If false, the space for the branch arrow will be collapsed.
*/
hasContentBranch: NO,
/**
(displayDelegate) The name of the property used for the checkbox value.
The checkbox will only be visible if this key is not null.
@type {String}
*/
contentCheckboxKey: null,
/**
(displayDelegate) Property key to use for the icon url
This property will be checked on the content object to determine the
icon to display. It must return either a URL or a CSS class name.
*/
contentIconKey: null,
/**
(displayDelegate) Property key to use for the right icon url
This property will be checked on the content object to determine the
icon to display. It must return either a URL or a CSS class name.
*/
contentRightIconKey: null,
/**
(displayDelegate) The name of the property used for label itself
If null, then the content object itself will be used..
*/
contentValueKey: null,
/**
IF true, the label value will be escaped to avoid HTML injection attacks.
You should only disable this option if you are sure you will only
display content that is already escaped and you need the added
performance gain.
*/
escapeHTML: YES,
/**
(displayDelegate) The name of the property used to find the count of
unread items.
The count will only be visible if this property is not null and the
returned value is not 0.
*/
contentUnreadCountKey: null,
/**
(displayDelegate) The name of the property used to determine if the item
is a branch or leaf (i.e. if the branch icon should be displayed to the
right edge.)
If this is null, then the branch view will be completely hidden.
Otherwise space will be allocated for it.
*/
contentIsBranchKey: null,
/**
YES if the item view is currently editing.
*/
isEditing: NO,
/**
Indent to use when rendering a list item with an outline level > 0. The
left edge of the list item will be indented by this amount for each
outline level.
*/
outlineIndent: 16,
/**
Outline level for this list item. Usually set by the collection view.
*/
outlineLevel: 0,
/**
Disclosure state for this list item. Usually set by the collection view
when the list item is created.
*/
disclosureState: SC.LEAF_NODE,
contentPropertyDidChange: function() {
//if (this.get('isEditing')) this.discardEditing() ;
if (this.get('contentIsEditable') !== this.contentIsEditable()) {
this.notifyPropertyChange('contentIsEditable');
}
this.displayDidChange();
},
/**
Determines if content is editable or not. Checkboxes and other related
components will render disabled if an item is not editable.
*/
contentIsEditable: function() {
var content = this.get('content');
return content && (content.get('isEditable')!==NO);
}.property('content').cacheable(),
/**
Fills the passed html-array with strings that can be joined to form the
innerHTML of the receiver element. Also populates an array of classNames
to set on the outer element.
@param {SC.RenderContext} context
@param {Boolean} firstTime
@returns {void}
*/
render: function(context, firstTime) {
var content = this.get('content'),
del = this.displayDelegate,
level = this.get('outlineLevel'),
indent = this.get('outlineIndent'),
key, value, working ;
// add alternating row classes
context.addClass((this.get('contentIndex')%2 === 0) ? 'even' : 'odd');
context.setClass('disabled', !this.get('isEnabled'));
// outline level wrapper
working = context.begin("div").addClass("sc-outline");
if (level>=0 && indent>0) working.addStyle("left", indent*(level+1));
// handle disclosure triangle
value = this.get('disclosureState');
if (value !== SC.LEAF_NODE) {
this.renderDisclosure(working, value);
context.addClass('has-disclosure');
}
// handle checkbox
key = this.getDelegateProperty('contentCheckboxKey', del) ;
if (key) {
value = content ? (content.get ? content.get(key) : content[key]) : NO ;
this.renderCheckbox(working, value);
context.addClass('has-checkbox');
}
// handle icon
if (this.getDelegateProperty('hasContentIcon', del)) {
key = this.getDelegateProperty('contentIconKey', del) ;
value = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
this.renderIcon(working, value);
context.addClass('has-icon');
}
// handle label -- always invoke
key = this.getDelegateProperty('contentValueKey', del) ;
value = (key && content) ? (content.get ? content.get(key) : content[key]) : content ;
if (value && SC.typeOf(value) !== SC.T_STRING) value = value.toString();
if (this.get('escapeHTML')) value = SC.RenderContext.escapeHTML(value);
this.renderLabel(working, value);
// handle right icon
if (this.getDelegateProperty('hasContentRightIcon', del)) {
key = this.getDelegateProperty('contentRightIconKey', del) ;
value = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
this.renderRightIcon(working, value);
context.addClass('has-right-icon');
}
// handle unread count
key = this.getDelegateProperty('contentUnreadCountKey', del) ;
value = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (!SC.none(value) && (value !== 0)) {
this.renderCount(working, value) ;
var digits = ['zero', 'one', 'two', 'three', 'four', 'five'];
var digit = (value.toString().length < digits.length) ? digits[value.toString().length] : digits[digits.length-1];
context.addClass('has-count %@-digit'.fmt(digit));
}
// handle action
key = this.getDelegateProperty('listItemActionProperty', del) ;
value = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (value) {
this.renderAction(working, value);
context.addClass('has-action');
}
// handle branch
if (this.getDelegateProperty('hasContentBranch', del)) {
key = this.getDelegateProperty('contentIsBranchKey', del);
value = (key && content) ? (content.get ? content.get(key) : content[key]) : NO ;
this.renderBranch(working, value);
context.addClass('has-branch');
}
context = working.end();
},
/**
Adds a disclosure triangle with the appropriate display to the content.
This method will only be called if the disclosure state of the view is
something other than SC.LEAF_NODE.
@param {SC.RenderContext} context the render context
@param {Boolean} state YES, NO or SC.MIXED_STATE
@returns {void}
*/
renderDisclosure: function(context, state) {
var key = (state === SC.BRANCH_OPEN) ? "open" : "closed",
cache = this._scli_disclosureHtml,
html, tmp;
if (!cache) cache = this.constructor.prototype._scli_disclosureHtml = {};
html = cache[key];
if (!html) {
html = cache[key] = '<img src="%@" class="disclosure button %@" />'.fmt(SC.BLANK_IMAGE_URL, key);
}
context.push(html);
},
/**
Adds a checkbox with the appropriate state to the content. This method
will only be called if the list item view is supposed to have a
checkbox.
@param {SC.RenderContext} context the render context
@param {Boolean} state YES, NO or SC.MIXED_STATE
@returns {void}
*/
renderCheckbox: function(context, state) {
var key = (state === SC.MIXED_STATE) ? "mixed" : state ? "sel" : "nosel",
cache = this._scli_checkboxHtml,
isEnabled = this.get('contentIsEditable') && this.get('isEnabled'),
html, tmp;
if (!isEnabled) key = SC.keyFor('disabled', key);
if (!cache) cache = this.constructor.prototype._scli_checkboxHtml = {};
html = cache[key];
if (!html) {
tmp = SC.RenderContext('a').attr('href', 'javascript:;')
.classNames(SC.CheckboxView.prototype.classNames);
// set state on html
if (state === SC.MIXED_STATE) tmp.addClass('mixed');
else tmp.setClass('sel', state);
// disabled
tmp.setClass('disabled', !isEnabled);
// now add inner content. note we do not add a real checkbox because
// we don't want to have to setup a change observer on it.
tmp.push('<span class="button"></span>');
// apply edit
html = cache[key] = tmp.join();
}
context.push(html);
},
/**
Generates an icon for the label based on the content. This method will
only be called if the list item view has icons enabled. You can override
this method to display your own type of icon if desired.
@param {SC.RenderContext} context the render context
@param {String} icon a URL or class name.
@returns {void}
*/
renderIcon: function(context, icon){
// get a class name and url to include if relevant
var url = null, className = null ;
if (icon && SC.ImageView.valueIsUrl(icon)) {
url = icon; className = '' ;
} else {
className = icon; url = SC.BLANK_IMAGE_URL ;
}
// generate the img element...
context.begin('img')
.addClass('icon').addClass(className)
.attr('src', url)
.end();
},
/**
Generates a label based on the content. You can override this method to
display your own type of icon if desired.
@param {SC.RenderContext} context the render context
@param {String} label the label to display, already HTML escaped.
@returns {void}
*/
renderLabel: function(context, label) {
context.push('<label>', label || '', '</label>') ;
},
/**
Finds and retrieves the element containing the label. This is used
for inline editing. The default implementation returns a CoreQuery
selecting any label elements. If you override renderLabel() you
probably need to override this as well.
@returns {SC.CoreQuery} CQ object selecting label elements
*/
$label: function() {
return this.$('label') ;
},
/**
Generates a right icon for the label based on the content. This method will
only be called if the list item view has icons enabled. You can override
this method to display your own type of icon if desired.
@param {SC.RenderContext} context the render context
@param {String} icon a URL or class name.
@returns {void}
*/
renderRightIcon: function(context, icon){
// get a class name and url to include if relevant
var url = null, className = null ;
if (icon && SC.ImageView.valueIsUrl(icon)) {
url = icon; className = '' ;
} else {
className = icon; url = SC.BLANK_IMAGE_URL ;
}
// generate the img element...
context.begin('img')
.addClass('right-icon').addClass(className)
.attr('src', url)
.end();
},
/**
Generates an unread or other count for the list item. This method will
only be called if the list item view has counts enabled. You can
override this method to display your own type of counts if desired.
@param {SC.RenderContext} context the render context
@param {Number} count the count
@returns {void}
*/
renderCount: function(context, count) {
context.push('<span class="count"><span class="inner">')
.push(count.toString()).push('</span></span>') ;
},
/**
Generates the html string used to represent the action item for your
list item. override this to return your own custom HTML
@param {SC.RenderContext} context the render context
@param {String} actionClassName the name of the action item
@returns {void}
*/
renderAction: function(context, actionClassName){
context.push('<img src="',SC.BLANK_IMAGE_URL,'" class="action" />');
},
/**
Generates the string used to represent the branch arrow. override this to
return your own custom HTML
@param {SC.RenderContext} context the render context
@param {Boolean} hasBranch YES if the item has a branch
@returns {void}
*/
renderBranch: function(context, hasBranch) {
context.begin('span').addClass('branch')
.addClass(hasBranch ? 'branch-visible' : 'branch-hidden')
.push(' ').end();
},
/**
Determines if the event occured inside an element with the specified
classname or not.
*/
_isInsideElementWithClassName: function(className, evt) {
var layer = this.get('layer');
if (!layer) return NO ; // no layer yet -- nothing to do
var el = SC.$(evt.target) ;
var ret = NO, classNames ;
while(!ret && el.length>0 && (el.get(0) !== layer)) {
if (el.hasClass(className)) ret = YES ;
el = el.parent() ;
}
el = layer = null; //avoid memory leaks
return ret ;
},
/** @private
Returns YES if the list item has a checkbox and the event occurred
inside of it.
*/
_isInsideCheckbox: function(evt) {
var del = this.displayDelegate ;
var checkboxKey = this.getDelegateProperty('contentCheckboxKey', del) ;
return checkboxKey && this._isInsideElementWithClassName('sc-checkbox-view', evt);
},
/** @private
Returns YES if the list item has a disclosure triangle and the event
occurred inside of it.
*/
_isInsideDisclosure: function(evt) {
if (this.get('disclosureState')===SC.LEAF_NODE) return NO;
return this._isInsideElementWithClassName('disclosure', evt);
},
/** @private
mouseDown is handled only for clicks on the checkbox view or or action
button.
*/
mouseDown: function(evt) {
// if content is not editable, then always let collection view handle the
// event.
if (!this.get('contentIsEditable')) return NO ;
// if occurred inside checkbox, item view should handle the event.
if (this._isInsideCheckbox(evt)) {
this._addCheckboxActiveState() ;
this._isMouseDownOnCheckbox = YES ;
this._isMouseInsideCheckbox = YES ;
return YES ; // listItem should handle this event
} else if (this._isInsideDisclosure(evt)) {
this._addDisclosureActiveState();
this._isMouseDownOnDisclosure = YES;
this._isMouseInsideDisclosure = YES ;
return YES;
}
return NO ; // let the collection view handle this event
},
mouseUp: function(evt) {
var ret= NO, del, checkboxKey, content, state, idx, set;
// if mouse was down in checkbox -- then handle mouse up, otherwise
// allow parent view to handle event.
if (this._isMouseDownOnCheckbox) {
// update only if mouse inside on mouse up...
if (this._isInsideCheckbox(evt)) {
del = this.displayDelegate ;
checkboxKey = this.getDelegateProperty('contentCheckboxKey', del);
content = this.get('content') ;
if (content && content.get) {
var value = content.get(checkboxKey) ;
value = (value === SC.MIXED_STATE) ? YES : !value ;
content.set(checkboxKey, value) ; // update content
this.displayDidChange(); // repaint view...
}
}
this._removeCheckboxActiveState() ;
ret = YES ;
// if mouse as down on disclosure -- handle mosue up. otherwise pass on
// to parent.
} else if (this._isMouseDownOnDisclosure) {
if (this._isInsideDisclosure(evt)) {
state = this.get('disclosureState');
idx = this.get('contentIndex');
set = (!SC.none(idx)) ? SC.IndexSet.create(idx) : null;
del = this.get('displayDelegate');
if (state === SC.BRANCH_OPEN) {
if (set && del && del.collapse) del.collapse(set);
else this.set('disclosureState', SC.BRANCH_CLOSED);
this.displayDidChange();
} else if (state === SC.BRANCH_CLOSED) {
if (set && del && del.expand) del.expand(set);
else this.set('disclosureState', SC.BRANCH_OPEN);
this.displayDidChange();
}
}
this._removeDisclosureActiveState();
ret = YES ;
}
// clear cached info
this._isMouseInsideCheckbox = this._isMouseDownOnCheckbox = NO ;
this._isMouseDownOnDisclosure = this._isMouseInsideDisclosure = NO ;
return ret ;
},
mouseExited: function(evt) {
if (this._isMouseDownOnCheckbox) {
this._removeCheckboxActiveState() ;
this._isMouseInsideCheckbox = NO ;
} else if (this._isMouseDownOnDisclosure) {
this._removeDisclosureActiveState();
this._isMouseInsideDisclosure = NO ;
}
return NO ;
},
mouseEntered: function(evt) {
if (this._isMouseDownOnCheckbox) {
this._addCheckboxActiveState() ;
this._isMouseInsideCheckbox = YES ;
} else if (this._isMouseDownOnDisclosure) {
this._addDisclosureActiveState();
this._isMouseInsideDisclosure = YES;
}
return NO ;
},
_addCheckboxActiveState: function() {
var enabled = this.get('isEnabled');
this.$('.sc-checkbox-view').setClass('active', enabled);
},
_removeCheckboxActiveState: function() {
this.$('.sc-checkbox-view').removeClass('active');
},
_addDisclosureActiveState: function() {
var enabled = this.get('isEnabled');
this.$('img.disclosure').setClass('active', enabled);
},
_removeDisclosureActiveState: function() {
this.$('img.disclosure').removeClass('active');
},
/**
Returns true if a click is on the label text itself to enable editing.
Note that if you override renderLabel(), you probably need to override
this as well, or just $label() if you only want to control the element
returned.
@param evt {Event} the mouseUp event.
@returns {Boolean} YES if the mouse was on the content element itself.
*/
contentHitTest: function(evt) {
// if not content value is returned, not much to do.
var del = this.displayDelegate ;
var labelKey = this.getDelegateProperty('contentValueKey', del) ;
if (!labelKey) return NO ;
// get the element to check for.
var el = this.$label().get(0) ;
if (!el) return NO ; // no label to check for.
var cur = evt.target, layer = this.get('layer') ;
while(cur && (cur !== layer) && (cur !== window)) {
if (cur === el) return YES ;
cur = cur.parentNode ;
}
return NO;
},
beginEditing: function() {
if (this.get('isEditing')) return YES ;
//if (!this.get('contentIsEditable')) return NO ;
return this._beginEditing(YES);
},
_beginEditing: function(scrollIfNeeded) {
var content = this.get('content'),
del = this.get('displayDelegate'),
labelKey = this.getDelegateProperty('contentValueKey', del),
parent = this.get('parentView'),
pf = parent ? parent.get('frame') : null,
el = this.$label(),
f, v, offset, oldLineHeight, fontSize, top, lineHeight,
lineHeightShift, targetLineHeight, ret ;
// if possible, find a nearby scroll view and scroll into view.
// HACK: if we scrolled, then wait for a loop and get the item view again
// and begin editing. Right now collection view will regenerate the item
// view too often.
if (scrollIfNeeded && this.scrollToVisible()) {
var collectionView = this.get('owner'), idx = this.get('contentIndex');
this.invokeLater(function() {
var item = collectionView.itemViewForContentIndex(idx);
if (item && item._beginEditing) item._beginEditing(NO);
});
return YES; // let the scroll happen then begin editing...
}
// nothing to do...
if (!parent || !el || el.get('length')===0) return NO ;
v = (labelKey && content && content.get) ? content.get(labelKey) : null ;
f = this.computeFrameWithParentFrame(null);
offset = SC.viewportOffset(el[0]);
// if the label has a large line height, try to adjust it to something
// more reasonable so that it looks right when we show the popup editor.
oldLineHeight = el.css('lineHeight');
fontSize = el.css('fontSize');
top = this.$().css('top');
if (top) top = parseInt(top.substring(0,top.length-2),0);
else top =0;
lineHeight = oldLineHeight;
lineHeightShift = 0;
if (fontSize && lineHeight) {
targetLineHeight = fontSize * 1.5 ;
if (targetLineHeight < lineHeight) {
el.css({ lineHeight: '1.5' });
lineHeightShift = (lineHeight - targetLineHeight) / 2;
} else oldLineHeight = null ;
}
f.x = offset.x;
f.y = offset.y+top + lineHeightShift ;
f.height = el[0].offsetHeight ;
f.width = el[0].offsetWidth ;
ret = SC.InlineTextFieldView.beginEditing({
frame: f,
exampleElement: el,
delegate: this,
value: v,
multiline: NO,
isCollection: YES
}) ;
// restore old line height for original item if the old line height
// was saved.
if (oldLineHeight) el.css({ lineHeight: oldLineHeight }) ;
// Done! If this failed, then set editing back to no.
return ret ;
},
commitEditing: function() {
if (!this.get('isEditing')) return YES ;
return SC.InlineTextFieldView.commitEditing();
},
discardEditing: function() {
if (!this.get('isEditing')) return YES ;
return SC.InlineTextFieldView.discardEditing();
},
/** @private
Set editing to true so edits will no longer be allowed.
*/
inlineEditorWillBeginEditing: function(inlineEditor) {
this.set('isEditing', YES);
},
/** @private
Hide the label view while the inline editor covers it.
*/
inlineEditorDidBeginEditing: function(inlineEditor) {
var el = this.$label() ;
this._oldOpacity = el.css('opacity');
el.css('opacity', 0.0) ;
},
/** @private
Could check with a validator someday...
*/
inlineEditorShouldEndEditing: function(inlineEditor, finalValue) {
return YES ;
},
/** @private
Update the field value and make it visible again.
*/
inlineEditorDidEndEditing: function(inlineEditor, finalValue) {
this.set('isEditing', NO) ;
var content = this.get('content') ;
var del = this.displayDelegate ;
var labelKey = this.getDelegateProperty('contentValueKey', del) ;
if (labelKey && content && content.set) {
content.set(labelKey, finalValue) ;
}
this.displayDidChange();
}
});