-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathHopscotchForHTML5.ns
4783 lines (4560 loc) · 154 KB
/
HopscotchForHTML5.ns
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
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Newspeak3
'Root'
class HopscotchForHTML5 usingPlatform: p images: is ducts: ds (* :exemplar: platform hopscotch *) = (
(* Hopscotch is Newspeak's UI framework. This version is implemented on top of HTML and CSS.
Copyright Google Inc. 2012 -2017
Copyright Ryan Macnak, Gilad Bracha 2018-2022
Copyright Gilad Bracha 2023-2024
This class defines a UI framework of the Newspeak platform on the web. Of course, one can use the DOM directly (as the code in this class does in its lower layers) or call any desired Javascript framework; but these are unpleasant, and we'd rather pave over the web's native horrors.
The classes herein can be classified into:
Windows/Shells
Fragments
- Presenters
- Composers
Navigation/History
Decorators
The entire process is driven by the window/shell classes. Normally, there is a HopscotchWindow encompassing the entire browser tab. However, one can also embed Hopscotch within HTML, and for this we have EmbeddedHopscotchWindow. Both inherit key behavior from HopscotchShell.
The key entry points are openSubject:, enterSubject:, enterPresenter: and displayPresenter:; these are all methods of the window/shell classes. They call each other in the order listed above. Ultimately, displayPresenter: handles the insertion of a presenter's DOM tree into the actual DOM, so the browser will display it.
Fragments are logical view elements. In Web circles, the fragment tree might be thought of as a shadow-DOM.
Base fragments are defined by Hopscotch and map into DOM elements. The DOM element of a fragment is called its visual. A presenter is a special kind of fragment - essentially a higher-level user-defined fragment. A presenter computes a fragment tree to represent it using its #definition method. This yields a tree of DOM elements that it then inserts into the browser's current document.
Presenters have a slot called #substanceSlot. The substance is the fragment tree for rendering the presenter. It gets initialized lazily by a method called #ensureSubstance. This method runs #definition to compute the fragment tree, hooks up the back pointer from the fragment tree to its parent (the presenter) and calls #noticeSubstanceCreation, which is some sort of hook that lets code respond to this situation. No code uses it.
When a presenter is to be displayed by a HopscotchShell, it is asked for its visual, which is its native widget tree. If the visual has already been computed, it is available in the slot #visualX inherited from Fragment. Otherwise, #createVisual is called, which in turn calls #ensureSubstance. This in turn will either invoke the #definition method to compute the fragment tree, or reuse the existing tree stored in #substanceSlot as noted above.
The effect will be to recursively produce a fragment tree and from it, a widget tree, caching these two so that they need not be recomputed at a later time (say, the next display cycle).
This version of Hopscotch is reactive.
UI actions that result in semantic changes must invoke #updateGUI: with a closure that takes the desired semantic action.
The call executes the closure and recomputes the state of the displayed fragment tree afresh based on the modified subjects (and their underlying models). This leads to update of the actual visual tree.
Other fragment trees (those tied to other presenters that are reachable via the UI) will be recomputed lazily when they are displayed.
The recomputation of fragments is ensured as follows: This class maintains a counter, #uiGeneration, which is incremented on every
call to #updateGUI:. When a presenter is created, it sets its #generation slot to the current value of #uiGeneration. When a subject is asked for its presenter and it has a cached presenter, it checks if the cached presenter's generation is less than #uiGeneration.
If so, it creates a new presenter and updates its cache.
Most reactive frameworks endeavour to optimize this process to avoid excess recomputation of the visual tree. Typically this
involves a diff of the new and old fragment trees, to detect where changes have actually occurred and only rebuild the visuals of
the modified parts. In our case, such a diff is needed not only as an optimization, but to deal with stateful presenters or fragments.
For example, if a node in a tree-view (such as a method presenter) is open, we need to ensure that it remains open upon refresh.
Likewise, if a node is being edited and the edit has not yet been saved, we must preserve the edited text.
In some systems, presenters are stateless, but this only moves the problem around. The above example remains necessary as a matter of good user experience; we can often move the state into the underlying application, but that state still needs to be managed.
The diff process is driven by the #updateVisualsFrom: method of Fragment which accepts an old fragment to compare against. If fragments are not of the same kind, a new visual is computed and attached to the DOM at the right place. To test whether fragments are of the same kind, they must implement #isMyKind:, which tests whether another fragment is of the same kind. The implementation is #isMyKind: is formulaic, calling #isKindOfX for whatever type X the presenter represents.
If the incoming fragment parameter is of the same kind as the receiver, #updateVisualsFrom: calls #updateFromSameKind: which Fragment subclasses must also implement.
The method #updateFromSameKind: is responsible for recomputing the visual for the fragment, utilizing any relevant information from the old fragment. Typically this means preserving any state that should be retained across refreshes. Where possible, part or all of the original visual is reused. Using the visual returned by #updateFromSameKind:, #updateVisualsFrom: updates the cached visual of the fragment.
In many cases, users of the framework can rely on the #updateFromSameKind: implementation of Presenter. However, this is not always the case. In that case, one may have to override #updateFromSameKind:.
In some cases, the presenter needs to check if the old presenter is *relevant* before deciding to update. Typically this is done by testing if the subjects are equal. It may be that this test will move into the framework. If the old presenter is not relevant, #updateFromSameKind: should return the new visual. In that case, it must ensure that this visual replaces the old one by calling #replaceVisual:with: (again, maybe the framework can take care of this?).
In any case where the new visual is returned, one must never recursively call #updateFromSameKind:. This supports the invariant that #updateFromSameKind: is only called when the old visual's parent is valid.
If the user code has interesting state that requires deeper semantic understanding to preserve, the overriding #updateFromSameKind: method should ensure that any state that needs to be maintained, in either the old presenter or the old subject, is captured in the new presenter (respectively, the new subject).
Ensuring that all built-in fragments implement this protocol correctly, and applying it to the existing presenter classes of the web IDE is an ongoing process. So far, we have not seen a substantial performance problem (at least on desktops). The main problems are preserving the state of presenters (the IDE presenters are mostly stateful for the reasons indicated above).
Moving forwards and backwards in the browser should display correctly updated views while preserving presenter state. Presenters
and subjects must be aware of the requirements imposed to support this requirement. See #SinglePageNavigationHistory and #NavigationHistory for a description.
See comments in Fragment, Presenter and Subject for more details.
Further documentation of subjects and decorators will be added in the future.
*)
|
(* Imports *)
private Color = p graphics Color.
private Context = p graphics Context.
private Point = p graphics Point.
private Holder = ds Holder.
private Font = p fonts Font.
private FontConfig = p fonts FontConfig.
private List = p collections List.
private Map = p collections Map.
private Set = p collections Set.
private TextBlock = p text TextBlock.
private TextProperties = p text TextProperties.
private TextString = p text TextString.
private Timer = p actors Timer.
private MessageNotUnderstood = p kernel MessageNotUnderstood.
(* JS and Web objects. These are initialized lazily because Hopscotch
is provided by some Newspeak platforms. As such, the module is
instantiated as part of the serialization process, before load time.
Hence JS objects are not yet available.
*)
private js = p js.
private window_slot
private document_slot
JSObject_slot
DOMParser_slot
private body_slot
private history_slot
(* Module variables (non-JS) *)
currentHopscotchWindow <HopscotchShell>
windowList <List[HopscotchShell]> = List new.
uiGeneration <Number> ::= 0.
private deferredContentQueue = List new.
(* should be lazy *)
(* Needs to be lazy because FontConfig precomputes maps.
Maps rely on hash values, which are not stable across serialization.
And again, this module gets serialized as part of platform objects that
support the Newspeak GUI.
*)
private fontConfig_slot
public core = self. (* Polymorphic with HopscotchForBrazil *)
public fragments = self. (* Polymorphic with HopscotchForBrazil *)
public homeSubjectClass ::= HomeSubject.
public useSurroundingNavigation <Boolean> ::= p isKindOfPlatformWithElectron not.
(* Re-export *)
public ducts = ds.
(* Images *)
images = is.
(* Style *)
public styleBorderColor <String> = 'silver'.
public styleButtonSize <Float> = 30.
public styleCheckboxSize = '36px'.
public styleColorPickerSize = '36px'.
public styleDefaultInset = '10px'.
public styleDefaultInterfaceTextColor = '#3C3C3C'.
public styleDefaultEditorTextColor = 'black'.
public styleDefaultRadius = '5px'.
public styleFontFamilyMonospace = 'ui-monospace, monospace'.
public styleFontFamilySansSerif = '-apple-system, sans-serif'.
public styleFontFamilySerif = 'serif'.
public styleFontSizeEditor = '14px'.
public styleFontSizeMenu = '13px'.
public styleFontSizeText = '14px'.
public styleMenuBackgroundColor = '#F6F6F6'.
public styleMenuBorderColor = '#C7C7C7'.
public styleMenuInset = '15px'.
public styleMenuItemHeight = '24px'.
public styleMenuShadowColor = 'darkgrey'.
public styleRowHeight = '30px'.
public styleTextInputHeight = '24px'.
public styleZebraPrimaryColor = Color white.
public styleZebraSecondaryColor = Color gray: 0.97.
|) (
class BlankFragment = LeafFragment (
) (
createVisual = (
^document createElement: 'div'
)
public isKindOfBlankFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfBlankFragment
)
updateVisualsFromSameKind: oldFragment <Fragment> ^ <Alien[Element]> = (
^oldFragment visual
)
) : (
)
class CheckboxFragment text: t <Holder> value: v <Boolean | Holder> action: a <[:Boolean]> = LeafFragment (
|
text <String> ::= t.
action <[:Boolean]> = a.
holder <Holder>
checked <Boolean>
subscription <Block>
checkbox <Alien[Element]>
|
v isKindOfHolder
ifTrue: [
holder:: v.
checked:: holder value.
subscription:: holder changed => [:value |
checked:: value.
checkbox at: 'checked' put: checked.
].
]
ifFalse: [
checked:: v
].
('created checkbox with checked = ', checked printString) out.
) (
public isKindOfCheckboxFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfCheckboxFragment
)
writeInput: v = (
holder isNil ifFalse: [
holder value: v.
].
action isNil ifFalse: [
action value: v.
].
)
createVisual = (
|
container <Alien[Div]> = document createElement: 'div'.
label <Alien[Element]> = document createTextNode: text.
|
container at: 'id' put: 'CheckboxContainer'.
(container at: 'style')
at: 'display' put: 'flex';
at: 'flex-direction' put: 'row';
at: 'align-items' put: 'center';
at: 'justify-content' put: 'flex-start'.
checkbox:: document createElement: 'input'.
checkbox
at: 'type' put: 'checkbox';
at: 'checked' put: checked;
at: 'oninput' put: inputHandler.
container appendChild: checkbox.
container appendChild: label.
(checkbox at: 'style')
at: 'min-width' put: styleCheckboxSize;
at: 'min-height' put: styleCheckboxSize;
at: 'margin-right' put: '5px'.
^container.
)
extractInput = (
^checked:: checkbox at: 'checked'.
)
updateVisualsFromSameKind: oldFragment <Fragment> ^ <Alien[Element]> = (
checkbox:: oldFragment visual.
checked:: checkbox at: 'checked'.
checkbox at: 'oninput' put: inputHandler.
^checkbox
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
('super input handler running with value' , extractInput printString) out.
writeInput: extractInput not.
false.
].
)
) : (
)
class ColorPickerFragment value: v <String | Holder> action: a <[:String]> = LeafFragment (
|
action <[:String]> = a.
holder <Holder>
color <String>
subscription <Block>
picker <Alien[Element]>
|
v isKindOfHolder
ifTrue: [
holder:: v.
color:: holder value.
subscription:: holder changed => [:value |
color:: value.
picker at: 'value' put: color.
].
]
ifFalse: [
color:: v
].
) (
public isKindOfColorPickerFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfColorPickerFragment
)
createVisual = (
picker:: document createElement: 'input'.
picker
at: 'id' put: 'ColorPicker';
at: 'type' put: 'color';
at: 'value' put: color;
at: 'oninput' put: inputHandler.
(picker at: 'style')
at: 'min-width' put: styleColorPickerSize;
at: 'min-height' put: styleColorPickerSize;
at: 'margin-right' put: '5px'.
^picker.
)
updateVisualsFromSameKind: oldFragment <ColorPickerFragment> ^ <Alien[Element]> = (
picker:: oldFragment visual.
picker at: 'oninput' put: inputHandler.
^picker
)
extractInput = (
^color:: picker at: 'value'.
)
writeInput: v = (
holder isNil ifFalse: [
holder value: color.
].
action isNil ifFalse: [
action value: color.
].
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false.
].
)
) : (
)
class DatePickerFragment value: v <String | Holder> action: a <[:String]> = LeafFragment (
|
action <[:String]> = a.
holder <Holder>
date <String>
subscription <Block>
picker <Alien[Element]>
|
v isKindOfHolder
ifTrue: [
holder:: v.
date:: holder value.
subscription:: holder changed => [:value |
date:: value.
picker at: 'value' put: date.
].
]
ifFalse: [
date:: v
].
) (
public isKindOfDatePickerFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfDatePickerFragment
)
createVisual = (
picker:: document createElement: 'input'.
picker
at: 'id' put: 'DatePicker';
at: 'type' put: 'date';
at: 'value' put: date;
at: 'oninput' put: inputHandler.
^picker.
)
updateVisualsFromSameKind: oldFragment <DatePickerFragment> ^ <Alien[Element]> = (
picker:: oldFragment visual.
picker at: 'value' put: date; at: 'oninput' put: inputHandler.
^picker
)
extractInput = (
^date:: picker at: 'value'.
)
writeInput: v = (
holder isNil ifFalse: [
holder value: date.
].
action isNil ifFalse: [
action value: date.
].
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false.
].
)
) : (
)
class PickerFragment text: t <String> items: i <List[String]> action: a <[:String]> = LeafFragment (
|
text <String> ::= t.
items <List[String]> = i.
action <[:Boolean]> = a.
item <String>
holder <Holder>
subscription <Block>
picker <Alien[Element]>
|
) (
public isKindOfPickerFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfPickerFragment
)
createVisual = (
|
container <Alien[Div]> = document createElement: 'div'.
label <Alien[Element]> = document createTextNode: text.
option <Alien[Element]>
|
container at: 'id' put: 'PickerContainer'.
(container at: 'style')
at: 'display' put: 'flex';
at: 'flex-direction' put: 'row';
at: 'align-items' put: 'center';
at: 'justify-content' put: 'flex-start'.
container appendChild: label.
picker:: document createElement: 'select'.
(picker at: 'style')
at: 'margin-left' put: '5px'.
picker
at: 'oninput' put: inputHandler.
container appendChild: picker.
items do: [:each |
option:: document createElement: 'option'.
option
at: 'value' put: each;
at: 'innerHTML' put: each.
picker appendChild: option.
].
^container.
)
updateVisualsFromSameKind: oldFragment <PickerFragment> ^ <Alien[Element]> = (
(* Too much trouble to replace closures, text and items - might as well use new visual *)
^replaceVisual: oldFragment visual with: visual
)
extractInput = (
^item:: picker at: 'value'.
)
writeInput: v = (
holder isNil ifFalse: [
holder value: item.
].
action isNil ifFalse: [
action value: item.
].
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false.
].
)
) : (
)
class ProgressBarFragment value: v <Float | Holder> = LeafFragment (
|
holder <Holder>
percent <Float>
public progress <Alien[Element]>
subscription <Block>
|
v isKindOfHolder
ifTrue: [
holder:: v.
percent:: holder value.
subscription:: holder changed => [:value |
percent:: value.
progress at: 'value' put: percent.
].
]
ifFalse: [
percent:: v asFloat.
].
) (
createVisual = (
progress:: document createElement: 'progress'.
progress at: 'value' put: percent.
^progress.
)
public isKindOfProgressBarFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfProgressBarFragment
)
updateVisualsFromSameKind: oldFragment <Fragment> ^ <Alien[Element]> = (
| oldVisual = oldFragment visual.|
progress:: oldFragment progress.
^oldVisual
)
) : (
)
class RadioButtonFragment
text: t <String>
value: v <Boolean | Holder>
group: g <String>
action: a <[:Boolean]> = LeafFragment (
|
text <String> ::= t.
group <String> = g.
action <[:Boolean]> = a.
holder <Holder>
checked <Boolean>
subscription <Block>
button <Alien[Element]>
|
v isKindOfHolder
ifTrue: [
holder:: v.
checked:: holder value.
subscription:: holder changed => [:value |
checked:: value.
button at: 'checked' put: checked.
].
]
ifFalse: [
checked:: v
].
) (
public isKindOfRadioButtonFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfRadioButtonFragment
)
createVisual = (
|
container <Alien[Div]> = document createElement: 'div'.
label <Alien[Element]> = document createTextNode: text.
|
container at: 'id' put: 'RadioButtonContainer'.
(container at: 'style')
at: 'display' put: 'flex';
at: 'flex-direction' put: 'row';
at: 'align-items' put: 'center';
at: 'justify-content' put: 'flex-start'.
button:: document createElement: 'input'.
button
at: 'type' put: 'radio';
at: 'checked' put: checked;
at: 'name' put: group;
at: 'oninput' put: inputHandler.
container appendChild: button.
container appendChild: label.
(button at: 'style')
at: 'min-width' put: styleCheckboxSize;
at: 'min-height' put: styleCheckboxSize;
at: 'margin-right' put: '5px'.
^container.
)
updateVisualsFromSameKind: oldFragment <RadioButtonFragment> ^ <Alien[Element]> = (
^replaceVisual: oldFragment visual with: visual
)
extractInput = (
^checked:: checked not
)
writeInput: v = (
holder isNil ifFalse: [
holder value: v.
].
action isNil ifFalse: [
action value: v.
].
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false
].
)
) : (
)
class RectangleFragment = BlankFragment (
|
rectangle <Alien[Element]>
|
style backgroundColor: Color black.
) (
createVisual = (
rectangle:: super createVisual.
applyStyle: rectangle.
(rectangle at: 'style')
at: 'cursor' put: 'default'.
registerGestures: rectangle.
^rectangle.
)
public isKindOfRectangleFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfRectangleFragment
)
public fill: color <Color> = (
style backgroundColor: color.
)
visualModified = (
applyStyle: rectangle.
)
updateVisualsFromSameKind: oldFragment <RectangleFragment> ^ <Alien[Element]> = (
^oldFragment visual
)
) : (
)
class SliderFragment value: v <Holder> min: mn <Float> max: mx <Float> = LeafFragment (
|
holder <Holder> = v.
min <Float> = mn.
max <Float> = mx.
|
) (
public isKindOfSliderFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfSliderFragment
)
createVisual = (
|
slider = document createElement: 'input'.
|
slider
at: 'type' put: 'range';
at: 'value' put: holder value;
at: 'min' put: min;
at: 'max' put: max;
at: 'oninput' put: inputHandler.
^slider.
)
updateVisualsFromSameKind: oldFragment <SliderFragment> ^ <Alien[Element]> = (
| slider = oldFragment visual. |
slider
at: 'value' put: state getValue;
at: 'min' put: min;
at: 'max' put: max; at: 'oninput' put: inputHandler.
^slider
)
extractInput = (
^Float parse: (visual at: 'value')
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false.
].
)
writeInput: v = (
holder value: v.
)
) : (
)
class Style = (
|
public backgroundColor <Color> ::= Color white.
public foregroundColor <Color> ::= Color black.
public borderType <String> ::= 'none'.
public borderColor <Color> ::= Color white.
public borderStyle <String> ::= 'solid'.
public borderWidth <Float> ::= 0.
public cornerRadius <Float> ::= 0.
public hasFontInfo <Boolean> ::= false.
|
) (
) : (
)
class ButtonStyle = Style(
backgroundColor:: Color gray: 0.97.
foregroundColor:: Color gray: 0.17.
cornerRadius:: 5.
) (
) : (
)
class ButtonFragment label: l action: a = LeafFragment (
|
public label = l.
public enabled ::= true.
action = a.
style <ButtonStyle> = ButtonStyle new.
|
) (
public isKindOfButtonFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfButtonFragment
)
createVisual = (
|
button = document createElement: 'button'.
|
button
appendChild: (document createTextNode: label);
at: 'onclick' put: [:event | action value. false];
at: 'disabled' put: enabled not;
at: 'contentEditable' put: 'false';
yourself.
applyStyle: button.
^button
)
updateVisualsFromSameKind: oldFragment <ButtonFragment> ^ <Alien[ButtonElement]> = (
| oldVisual <Alien[Element]> = oldFragment visual.|
oldVisual
at: 'onclick' put: [:event | action value. false];
at: 'disabled' put: enabled not.
oldFragment label ~= label ifTrue: [
oldVisual replaceChild: (document createTextNode: label) oldChild: (oldVisual at: 'firstChild')
].
^oldVisual
)
) : (
)
class TimePickerFragment value: v <String | Holder> action: a <[:String]> = LeafFragment (
|
action <[:String]> = a.
holder <Holder>
time <String>
subscription <Block>
picker <Alien[Element]>
|
v isKindOfHolder
ifTrue: [
holder:: v.
time:: holder value.
subscription:: holder changed => [:value |
time:: value.
picker at: 'value' put: time.
].
]
ifFalse: [
time:: v
].
) (
public isKindOfTimePickerFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfTimePickerFragment
)
createVisual = (
picker:: document createElement: 'input'.
picker
at: 'id' put: 'TimePicker';
at: 'type' put: 'time';
at: 'value' put: time;
at: 'oninput' put: inputHandler.
^picker.
)
updateVisualsFromSameKind: oldFragment <TimePickerFragment> ^ <Alien[Element]> = (
picker:: oldFragment visual.
picker at: 'value' put: time; at: 'oninput' put: inputHandler.
^picker
)
extractInput = (
^time:: picker at: 'value'.
)
writeInput: v = (
holder isNil ifFalse: [
holder value: time.
].
action isNil ifFalse: [
action value: time.
].
)
inputHandler ^ <[:Alien | Boolean]> = (
^[:event |
writeInput: extractInput.
false.
].
)
) : (
)
class CallBackWrapper wrapping: h <[:T :S]> = (
|
public callback <[:T :S]> ::= h.
|
) (
public wrappedCallback = (
^[:a1 :a2 | callback value: a1 value: a2]
)
) : (
)
class CanvasFragment withExtent: e = LeafFragment (
|
private alien = document createElement: 'canvas'.
|
alien at: 'width' put: e x.
alien at: 'height' put: e y.
(alien at: 'style') at: 'position' put: 'relative'.
) (
public context = (
^Context on: (alien getContext: '2d')
)
createVisual = (
^alien
)
public isKindOfCanvasFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfCanvasFragment
)
public keyDownAction: onKeyPressed <[:String]> = (
(* The canvas element itself doesn't seem to get key events. *)
body addEventListener: 'keydown' action:
[:event | onKeyPressed value: (String fromCharCode: (event at: 'keyCode')). nil].
)
public mouseDownAction: onMouseDown <[:Point]> = (
alien addEventListener: 'mousedown' action:
[:event | onMouseDown value: (event at: 'offsetX') @ (event at: 'offsetY'). nil].
)
public mouseMovedAction: onMouseMoved <[:Point]> = (
alien addEventListener: 'mousemove' action:
[:event | onMouseMoved value: (event at: 'offsetX') @ (event at: 'offsetY'). nil].
)
updateVisualsFromSameKind: oldFragment <CanvasFragment> ^ <Alien[Canvas]> = (
^oldFragment visual
)
) : (
)
public class CodeMirrorFragment onText: t <String> = LeafFragment (
|
public editor <Alien[CodeMirror]>
textSlot <TextFragment | String> ::= t.
public useEditControls <Boolean> ::= true.
public counterfactualBar <Alien[Span]>
public messageContainer <Alien[Span]>
public isInEditState ::= false.
public changeResponse <[TextEditorFragment]>
public acceptResponse <[TextEditorFragment]>
public cancelResponse <[TextEditorFragment]>
public evaluateResponse <[TextEditorFragment]>
styles ::= List new.
public beforeChangeHandler <CallBackWrapper>
public changeHandler <CallBackWrapper>
public keyHandler <CallBackWrapper>
public beforeSelectionChangeHandler <CallBackWrapper>
lastChangeWasSynthetic <Boolean> ::= false.
public readOnly <Boolean> ::= false.
|
) (
applyStyle: style <{Integer. Integer. Alien[JSObject]}> = (
| s <Alien[CodeMirrorPosition]> e <Alien[CodeMirrorPosition]> |
(* Newspeak intervals are 1-origin and [start, stop]. JS intervals are 0-origin and [start, stop). *)
s:: editor posFromIndex: (style at: 1) - 1.
e:: editor posFromIndex: (style at: 2).
editor markText: s to: e style: (style at: 3).
)
applyStyles = (
nil = editor ifFalse: [
styles do: [: s <{Integer. Integer. Alien[JSObject]}> |
applyStyle: s
].
styles:: List new.
editor refresh.
]
)
public defaultAcceptResponse = (
textSlot:: editor getValue.
leaveEditState
)
public defaultCancelResponse = (
editor setValue: textSlot.
leaveEditState
)
public defaultChangeResponse = (
updateEditState
)
defaultStyle ^ <Alien[Object]> = (
| style <Alien[JSObject]> = JSObject new. |
style at: 'css' put:
'color:', styleDefaultEditorTextColor, ';',
'font-family:', styleFontFamilySerif, ';',
'text-decoration: none; font-weight: normal'.
^style
)
public isKindOfCodeMirrorFragment ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfCodeMirrorFragment
)
public noticeExposure = (
refresh
)
public refresh = (
applyStyles
)
registerChangeHandler = (
changeHandler:: CallBackWrapper wrapping: [:codeMirror :change | respondToChange: change. nil].
editor on: 'change' respondToChange: changeHandler wrappedCallback.
)
registerKeyHandler = (
keyHandler:: CallBackWrapper wrapping: [:codeMirror :keydown | respondToKeyDown: codeMirror. nil].
editor on: 'keydown' respondToKeyDown: keyHandler wrappedCallback.
)
public resetStyles = (
hasVisual ifFalse: [^self].
editor markText: (editor posFromIndex: 0) to: (editor posFromIndex: textBeingAccepted size - 1) style: defaultStyle
)
respondToAccept: event <Alien[Event]> = (
nil = acceptResponse
ifTrue: [defaultAcceptResponse]
ifFalse: [acceptResponse cull: self cull: event]
)
respondToCancel = (
(*confirm: 'Confirm Cancel' ifConfirmed:
[*)nil = cancelResponse
ifTrue: [defaultCancelResponse]
ifFalse: [cancelResponse cull: self](*]*)
)
respondToEvaluate = (
nil = evaluateResponse
ifFalse: [evaluateResponse cull: self]
)
respondToBeforeChange: event <Alien[Event]> = (
updateEditState
)
respondToChange: change <Alien[CodeMirrorChangeObject]> = (
nil = changeResponse
ifTrue: [defaultChangeResponse]
ifFalse: [changeResponse cull: self cull: change].
)
public style: style <Alien[JSObject]> from: start <Integer> to: end <Integer> = (
styles add: {start. end. style}.
applyStyles
)
public text: t = (
textSlot:: t.
hasVisual ifTrue: [editor setValue: t].
)
public textBeingAccepted ^ <String> = (
^editor getValue
)
public updateEditState = (
lastChangeWasSynthetic ifTrue: [lastChangeWasSynthetic:: false. ^self].
isInEditState ifFalse:
[
visual.
useEditControls ifTrue: [
(counterfactualBar at: 'style')
at: 'opacity' put: '1.0'.
].
isInEditState:: true
].
)
public removeMessages = (
[visual removeChild: messageContainer] on: Error do: [:e | ].
)
public showMessage: message <String> = (
messageContainer at: 'innerHTML' put: message.
visual appendChild: messageContainer.
)
public hasPendingChanges ^<Boolean> = (
^isInEditState
)
public leaveEditState = (
isInEditState ifTrue:
[
useEditControls ifTrue: [
(counterfactualBar at: 'style')
at: 'opacity' put: '0.0'.
].
removeMessages.
isInEditState:: false.
]