File size: 34,858 Bytes
92f0e98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
labwidget by David Bau.

Base class for a lightweight javascript notebook widget framework
that is portable across Google colab and Jupyter notebooks.
No use of requirejs: the design uses all inline javascript.

Defines Model, Widget, Trigger, and Property, which set up data binding
using the communication channels available in either google colab
environment or jupyter notebook.

This module also defines Label, Textbox, Range, Choice, and Div
widgets; the code for these are good examples of usage of Widget,
Trigger, and Property objects.

Within HTML widgets, user interaction should update the javascript
model using model.set('propname', value); this will propagate to
the python model and notify any registered python listeners; similarly
model.on('propname', callback) will listen for property changes
that come from python.

TODO: Support jupyterlab also.
"""

import json, html, re
from inspect import signature

class Model(object):
    '''
    Abstract base class that supports data binding.  Within __init__,
    a model subclass defines databound events and properties using:

       self.evtname = Trigger()
       self.propname = Property(initval)

    Any Trigger or Property member can be watched by registering a
    listener with `model.on('propname', callback)`.

    An event can be triggered by `model.evtname.trigger(value)`.
    A property can be read with `model.propname`, and can be set by
    `model.propname = value`; this also triggers notifications.
    In both these cases, any registered listeners will be called
    with the given value.
    '''
    def on(self, name, cb):
        '''
        Registers a listener for named events and properties.
        A space-separated list of names can be provided as `name`.
        '''
        for n in name.split():
            self.prop(n).on(cb)
        return self

    def off(self, name, cb=None):
        '''
        Unregisters a listener for named events and properties.
        A space-separated list of names can be provided as `name`.
        '''
        for n in name.split():
            self.prop(n).off(cb)
        return self

    def prop(self, name):
        '''
        Returns the underlying Trigger or Property object for a
        property, rather than its held value.
        '''
        curvalue = super().__getattribute__(name)
        if not isinstance(curvalue, Trigger):
            raise AttributeError('%s not a property or trigger but %s'
                    % (name, str(type(curvalue))))
        return curvalue

    def _initprop_(self, name, value):
        '''
        To be overridden in base classes.  Handles initialization of
        a new Trigger or Property member.
        '''
        value.name = name
        value.target = self
        return

    def __setattr__(self, name, value):
        '''
        When a member is an Trigger or Property, then assignment notation
        is delegated to the Trigger or Property so that notifications
        and reparenting can be handled.  That is, `model.name = value`
        turns into `prop(name).set(value)`.
        '''
        if hasattr(self, name):
            curvalue = super().__getattribute__(name)
            if isinstance(curvalue, Trigger):
                # Delegte "set" to the underlying Property.
                curvalue.set(value)
            else:
                super().__setattr__(name, value)
        else:
            super().__setattr__(name, value)
            if isinstance(value, Trigger):
                self._initprop_(name, value)

    def __getattribute__(self, name):
        '''
        When a member is a Property, then property getter
        notation is delegated to the peoperty object.
        '''
        curvalue = super().__getattribute__(name)
        if isinstance(curvalue, Property):
            return curvalue.value
        return curvalue

class Widget(Model):
    '''
    Base class for an HTML widget that uses a Javascript model object
    to syncrhonize HTML view state with the backend Python model state.
    Each widget subclass overrides widget_js to provide Javascript code
    that defines the widget's behavior.  This javascript will be wrapped
    in an immediately-invoked function and included in the widget's HTML
    representation (_repr_html_) when the widget is viewed.

    A widget's javascript is provided with two local variables:

       element - the widget's root HTML element.  By default this is
                 a <div> but can be overridden in widget_html.
       model   - the object representing the data model for the widget.
                 within javascript.

    The model object provides the following javascript API:

       model.get('propname') obtains a current property value.
       model.set('propname', 'value') requests a change in value.
       model.on('propname', callback) listens for property changes.
       model.trigger('evtname', value) triggers an event.

    Note that model.set just requests a change but does not change the
    value immediately: model.get will not reflect the change until the
    python backend has handled it and notified the javascript of the new
    value, which will trigger any callbacks previously registered using
    .on('propname', callback).  Thus Widget impelements a V-shaped
    notification protocol:

    User entry ->                 |              -> User-visible feedback
        js model.set ->           |        -> js.model.on  callback
          python prop.trigger ->  |   -> python prop.notify
                         python prop.handle

    Finally, all widgets provide standard databinding for style and data
    properties, which are write-only (python-to-js) properties that
    let python directly control CSS styles and HTML dataset attributes
    for the top-level widget element.
    '''

    def __init__(self, style=None, data=None):
        # In the jupyter case, there can be some delay between js injection
        # and comm creation, so we need to queue some initial messages.
        if WIDGET_ENV == 'jupyter':
            self._comms = []
            self._queue = []
        # Each call to _repr_html_ creates a unique view instance.
        self._viewcount = 0
        # Python notification is handled by Property objects.
        def handle_remote_set(name, value):
            with capture_output(self): # make errors visible.
                self.prop(name).trigger(value)
        self._recv_from_js_(handle_remote_set)
        # The style and data properties come standard, and are used to
        # control the style and data attributes on the toplevel element.
        self.style = Property(style)
        self.data = Property(data)
        # Each widget has a "write" event that is used to insert
        # html before the widget.
        self.write = Trigger()

    def widget_js(self):
        '''
        Override to define the javascript logic for the widget.  Should
        render the initial view based on the current model state (if not
        already rendered using widget_html) and set up listeners to keep
        the model and the view synchornized.
        '''
        return ''

    def widget_html(self):
        '''
        Override to define the initial HTML view of the widget.  Should
        define an element with id given by view_id().
        '''
        return f'<div {self.std_attrs()}></div>'

    def view_id(self):
        '''
        Returns an HTML element id for the view currently being rendered.
        Note that each time _repr_html_ is called, this id will change.
        '''
        return f"_{id(self)}_{self._viewcount}"

    def std_attrs(self):
        '''
        Returns id and (if applicable) style attributes, escaped and
        formatted for use within the top-level element of widget HTML.
        '''
        return (f'id="{self.view_id()}"' +
                style_attr(self.style) +
                data_attrs(self.data))


    def _repr_html_(self):
        '''
        Returns the HTML code for the widget.
        '''
        self._viewcount += 1
        json_data = json.dumps({
                k: v.value for k, v in vars(self).items()
                if isinstance(v, Property)})
        json_data = re.sub('</', '<\\/', json_data)

        std_widget_js = minify(f'''
          var model = new Model("{id(self)}", {json_data});
          var element = document.getElementById("{self.view_id()}");
          model.on('write', (ev) => {{
            var dummy = document.createElement('div');
            dummy.innerHTML = ev.value.trim();
            dummy.childNodes.forEach((item) => {{
              element.parentNode.insertBefore(item, element);
            }});
          }});
          function upd(a) {{ return (e) => {{ for (k in e.value) {{
            element[a][k] = e.value[k];
          }}}}}}
          model.on('style', upd('style'));
          model.on('data', upd('dataset'));
        ''')

        return ''.join([
          self.widget_html(),
          '<script>(function() {',
          WIDGET_MODEL_JS,
          std_widget_js,
          self.widget_js(),
          '})();</script>'
        ]);

    def _initprop_(self, name, value):
        if not hasattr(self, '_viewcount'):
            raise ValueError('base Model __init__ must be called')
        super()._initprop_(name, value)
        def notify_js(event):
            self._send_to_js_(id(self), name, event.value)
        if isinstance(value, Trigger):
            value.on(notify_js, internal=True)

    def _send_to_js_(self, *args):
        if self._viewcount > 0:
            if WIDGET_ENV == 'colab':
                colab_output.eval_js(minify(f"""
                (window.send_{id(self)} = window.send_{id(self)} ||
                new BroadcastChannel("channel_{id(self)}")
                ).postMessage({json.dumps(args)});
                """), ignore_result=True)
            elif WIDGET_ENV == 'jupyter':
                if not self._comms:
                    self._queue.append(args)
                    return
                for comm in self._comms:
                    comm.send(args)

    def _recv_from_js_(self, fn):
        if WIDGET_ENV == 'colab':
            colab_output.register_callback(f"invoke_{id(self)}", fn)
        elif WIDGET_ENV == 'jupyter':
            def handle_comm(msg):
                fn(*(msg['content']['data']))
                # TODO: handle closing also.
            def handle_close(close_msg):
                comm_id = close_msg['content']['comm_id']
                self._comms = [c for c in self._comms if c.comm_id != comm_id]
            def open_comm(comm, open_msg):
                self._comms.append(comm)
                comm.on_msg(handle_comm)
                comm.on_close(handle_close)
                comm.send('ok')
                if self._queue:
                    for args in self._queue:
                        comm.send(args)
                    self._queue.clear()
                if open_msg['content']['data']:
                    handle_comm(open_msg)
            cname = "comm_" + str(id(self))
            COMM_MANAGER.register_target(cname, open_comm)

    def display(self):
        from IPython.core.display import display
        display(self)
        return self

class Trigger(object):
    """
    Trigger is the base class for Property and other data-bound
    field objects.  Trigger holds a list of listeners that need to
    be notified about the event.

    Multple Trigger objects can be tied (typically a parent Model can
    have Triggers that are triggered by children models).  To support
    this, each Trigger can have a parent.

    Trigger objects provide a notification protocol where view
    interactions trigger events at a leaf that are sent up to the
    root Trigger to be handled.  By default, the root handler accepts
    events by notifying all listeners and children in the tree.
    """
    def __init__(self):
        self._listeners = []
        self.parent = None
        # name and target are set in Model._initprop_.
        self.name = None
        self.target = None
    def handle(self, value):
        '''
        Method to override; called at the root when an event has been
        triggered, and on a child when the parent has notified.  By
        default notifies all listeners.
        '''
        self.notify(value)
    def trigger(self, value=None):
        '''
        Triggers an event to be handled by the root.  By default, the root
        handler will accept the event so all the listeners will be notified.
        '''
        if self.parent is not None:
            self.parent.trigger(value)
        else:
            self.handle(value)
    def set(self, value):
        '''
        Sets the parent Trigger.  Child Triggers trigger events by
        triggering parents, and in turn they handle notifications
        that come from parents.
        '''
        if self.parent is not None:
            self.parent.off(self.handle)
            self.parent = None
        if isinstance(value, Trigger):
            ancestor = value.parent
            while ancestor is not None:
                if ancestor == self:
                    raise ValueError('bound properties should not form a loop')
                ancestor = ancestor.parent
            self.parent = value
            self.parent.on(self.handle, internal=True)
        elif not isinstance(self, Property):
            raise ValueError('only properties can be set to a value')
    def notify(self, value=None):
        '''
        Notifies listeners and children.  If a listener accepts an argument,
        the value will be passed as a single argument.
        '''
        for cb, internal in self._listeners:
            with enter_handler(self.name, internal) as ctx:
                if ctx.silence:
                    # do not notify recursively...
                    # print(f'silenced recursive {self.name} {cb.__name__}')
                    pass
                elif len(signature(cb).parameters) == 0:
                    cb() # no-parameter callback.
                else:
                    cb(Event(value, self.name, self.target))
    def on(self, cb, internal=False):
        '''
        Registers a listener.  Calling multiple times registers
        multiple listeners.
        '''
        self._listeners.append((cb, internal))
    def off(self, cb=None):
        '''
        Unregisters a listener.
        '''
        self._listeners = [(c, i) for c, i in self._listeners
                if c != cb and cb is not None]

class Property(Trigger):
    """
    A Property is just an Trigger that remembers its last value.
    """
    def __init__(self, value=None):
        '''
        Can be initialized with a starting value.
        '''
        super().__init__()
        self.set(value)
    def handle(self, value):
        '''
        The default handling for a Property is to store the value,
        then notify listeners.  This method can be overridden,
        for example to validate values.
        '''
        self.value = value
        self.notify(value)
    def set(self, value):
        '''
        When a Property value is set to an ordinary value, it
        triggers an event which causes a notification to be
        sent to update all linked Properties.  A Property set
        to another Property becomes a child of the value.
        '''
        # Handle setting a parent Property
        if isinstance(value, Property):
            super().set(value)
            self.handle(value.value)
        elif isinstance(value, Trigger):
            raise ValueError('Cannot set a Property to an Trigger')
        else:
            self.trigger(value)

class Event(object):
    def __init__(self, value, name, target, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)
        self.value = value
        self.name = name
        self.target = target

entered_handler_stack = []
class enter_handler(object):
    def __init__(self, name, internal):
        global entered_handler_stack
        self.internal = internal
        self.name = name
        self.silence = (not internal) and (len(entered_handler_stack) > 0)
    def __enter__(self):
        global entered_handler_stack
        if not self.internal:
            entered_handler_stack.append(self)
        return self
    def __exit__(self, exc_type, exc_value, exc_tb):
        global entered_handler_stack
        if not self.internal:
            entered_handler_stack.pop()

class capture_output(object):
    """Context manager for capturing stdout/stderr.  This is used,
    by default, to wrap handler code that is invoked by a triggering
    event coming from javascript.  Any stdout/stderr or exceptions
    that are thrown are formatted and written above the relevant widget."""
    def __init__(self, widget):
        from io import StringIO
        self.widget = widget
        self.buffer = StringIO()
    def __enter__(self):
        import sys
        self.saved = dict(stdout=sys.stdout, stderr=sys.stderr)
        sys.stdout = self.buffer
        sys.stderr = self.buffer
    def __exit__(self, exc_type, exc_value, exc_tb):
        import sys, traceback
        captured = self.buffer.getvalue()
        if len(captured):
            self.widget.write.trigger(f'<pre>{html.escape(captured)}</pre>')
        if exc_type:
            import traceback
            tbtxt = ''.join(
                    traceback.format_exception(exc_type, exc_value, exc_tb))
            self.widget.write.trigger(
                    f'<pre style="color:red;text-align:left">{tbtxt}</pre>')
        sys.stdout = self.saved['stdout']
        sys.stderr = self.saved['stderr']


##########################################################################
## Specific widgets
##########################################################################

class Button(Widget):
    def __init__(self, label='button', style=None, **kwargs):
        super().__init__(style=defaulted(style, display='block'), **kwargs)
        self.click = Trigger()
        self.label = Property(label)
    def widget_js(self):
        return minify('''
          element.addEventListener('click', (e) => {
            model.trigger('click');
          })
          model.on('label', (ev) => {
            element.value = ev.value;
          })
        ''')
    def widget_html(self):
        return f'''<input {self.std_attrs()} type="button" value="{
            html.escape(str(self.label))}">'''

class Label(Widget):
    def __init__(self, value='', **kwargs):
        super().__init__(**kwargs)
        # databinding is defined using Property objects.
        self.value = Property(value)

    def widget_js(self):
        # Both "model" and "element" objects are defined within the scope
        # where the js is run.    "element" looks for the element with id
        # self.view_id(); if widget_html is overridden, this id should be used.
        return minify('''
            model.on('value', (ev) => {
                element.innerText = model.get('value');
            });
        ''')
    def widget_html(self):
        return f'''<label {self.std_attrs()}>{
            html.escape(str(self.value))}</label>'''

class Textbox(Widget):
    def __init__(self, value='', size=20, style=None, desc=None, **kwargs):
        super().__init__(style=defaulted(style, display='inline-block'), **kwargs)
        # databinding is defined using Property objects.
        self.value = Property(value)
        self.size = Property(size)
        self.desc = Property(desc)

    def widget_js(self):
        # Both "model" and "element" objects are defined within the scope
        # where the js is run.    "element" looks for the element with id
        # self.view_id(); if widget_html is overridden, this id should be used.
        return minify('''
          element.value = model.get('value');
          element.size = model.get('size');
          element.addEventListener('keydown', (e) => {
            if (e.code == 'Enter') {
              model.set('value', element.value);
            }
          });
          element.addEventListener('blur', (e) => {
            model.set('value', element.value);
          });
          model.on('value', (ev) => {
            element.value = model.get('value');
          });
          model.on('size', (ev) => {
            element.size = model.get('size');
          });
        ''')
    def widget_html(self):

        html_str = f'''<input {self.std_attrs()} value="{
            html.escape(str(self.value))}" size="{self.size}">'''
        if self.desc is not None:
            html_str = f"""<span>{self.desc}</span>{html_str}"""
        return html_str

class Range(Widget):
    def __init__(self, value=50, min=0, max=100, **kwargs):
        super().__init__(**kwargs)
        # databinding is defined using Property objects.
        self.value = Property(value)
        self.min = Property(min)
        self.max = Property(max)

    def widget_js(self):
        # Note that the 'input' event would enable during-drag feedback,
        # but this is pretty slow on google colab.
        return minify('''
          element.addEventListener('change', (e) => {
            model.set('value', element.value);
          });
          model.on('value', (e) => {
            if (!element.matches(':active')) {
              element.value = e.value;
            }
          })
        ''')
    def widget_html(self):
        return f'''<input {self.std_attrs()} type="range" value="{
            self.value}" min="{self.min}" max="{self.max}">'''

class Choice(Widget):
    """
    A set of radio button choices.
    """
    def __init__(self, choices=None, selection=None, horizontal=False,
            **kwargs):
        super().__init__(**kwargs)
        if choices is None:
            choices = []
        self.choices = Property(choices)
        self.horizontal = Property(horizontal)
        self.selection = Property(selection)
    def widget_js(self):
        # Note that the 'input' event would enable during-drag feedback,
        # but this is pretty slow on google colab.
        return minify('''
          function esc(unsafe) {
            return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;")
                   .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
          }
          function render() {
            var lines = model.get('choices').map((c) => {
              return '<label><input type="radio" name="choice" value="' +
                 esc(c) + '">' + esc(c) + '</label>'
            });
            element.innerHTML = lines.join(model.get('horizontal')?' ':'<br>');
          }
          model.on('choices horizontal', render);
          model.on('selection', (ev) => {
            [...element.querySelectorAll('input')].forEach((e) => {
              e.checked = (e.value == ev.value);
            })
          });
          element.addEventListener('change', (e) => {
            model.set('selection', element.choice.value);
          });
        ''')
    def widget_html(self):
        radios = [
            f"""<label><input name="choice" type="radio" {
            'checked' if value == self.selection else ''
            } value="{html.escape(value)}">{html.escape(value)}</label>"""
            for value in self.choices ]
        sep = " " if self.horizontal else "<br>"
        return f'<form {self.std_attrs()}>{sep.join(radios)}</form>'

class Menu(Widget):
    """
    A dropdown choice.
    """
    def __init__(self, choices=None, selection=None, **kwargs):
        super().__init__(**kwargs)
        if choices is None:
            choices = []
        self.choices = Property(choices)
        self.selection = Property(selection)
    def widget_js(self):
        return minify('''
          function esc(unsafe) {
            return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;")
                   .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
          }
          function render() {
            var selection = model.get('selection');
            var lines = model.get('choices').map((c) => {
              return '<option value="' + esc(''+c) + '"' +
                     (c == selection ? ' selected' : '') +
                     '>' + esc(''+c) + '</option>';
            });
            element.menu.innerHTML = lines.join('\\n');
          }
          model.on('choices horizontal', render);
          model.on('selection', (ev) => {
            [...element.querySelectorAll('option')].forEach((e) => {
              e.selected = (e.value == ev.value);
            })
          });
          element.addEventListener('change', (e) => {
            model.set('selection', element.menu.value);
          });
        ''')
    def widget_html(self):
        options = [
            f"""<option value="{html.escape(str(value))}" {
            'selected' if value == self.selection else ''
            }>{html.escape(str(value))}</option>"""
            for value in self.choices ]
        sep = "\n"
        return f'''<form {self.std_attrs()}"><select name="menu">{
             sep.join(options)}</select></form>'''

class Datalist(Widget):
    """
    An input with a dropdown choice.
    """
    def __init__(self, choices=None, value=None, **kwargs):
        super().__init__(**kwargs)
        if choices is None:
            choices = []
        self.choices = Property(choices)
        self.value = Property(value)
    def datalist_id(self):
        return self.view_id() + '-dl'
    def widget_js(self):
        # The mousedown/mouseleave dance defeats the prefix-matching behavior
        # of the built-in datalist by erasing value momentarily on mousedown.
        return minify('''
          function esc(unsafe) {
            return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;")
                   .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
          }
          function render() {
            var lines = model.get('choices').map((c) => {
              return '<option value="' + esc(''+c) + '">';
            });
            element.inp.list.innerHTML = lines.join('\\n');
          }
          model.on('choices', render);
          model.on('value', (ev) => {
            element.inp.value = ev.value;
          });
          function restoreValue() {
            var inp = element.inp;
            if (inp.value == '') {
              inp.value = inp.placeholder;
              inp.placeholder = '';
            }
          }
          element.inp.addEventListener('mousedown', (e) => {
            var inp = element.inp;
            if (inp.value != '') {
              inp.placeholder = inp.value;
              inp.value = '';
              if (e.clientX < inp.getBoundingClientRect().right - 25) {
                setTimeout(restoreValue, 0);
              }
            }
          });
          element.inp.addEventListener('mouseleave', restoreValue)
          element.inp.addEventListener('change', (e) => {
            model.set('value', element.inp.value);
          });
        ''')
    def widget_html(self):
        options = [
            f"""<option value="{html.escape(str(value))}">"""
            for value in self.choices ]
        return ''.join([
          f'<form {self.std_attrs()} onsubmit="return false;">',
          f'<input name="inp" list="{self.datalist_id()}" autocomplete="off">',
          f'<datalist id="{self.datalist_id()}">',
          ''.join(options),
          f'</datalist></form>'])

class Div(Widget):
    """
    Just an empty DIV element.  Use the innerHTML property to
    change its contents, or use the clear() and print() method.
    """
    def __init__(self, innerHTML='', **kwargs):
        super().__init__(**kwargs)
        # TODO: unify more closely with the show() library.
        self.innerHTML = Property(innerHTML)

    def clear(self):
        """Clears the contents of the div."""
        self.innerHTML = ''

    def show(self, *args):
        from . import show
        self.innerHTML = show.html(args)

    def print(self, *args, replace=False):
        """Appends plain text (as a pre) into the div."""
        newHTML = '<pre>%s</pre>' % ' '.join(
                html.escape(str(text)) for text in args)
        if replace:
            self.innerHTML = newHTML
        else:
            self.innerHTML += newHTML

    def widget_js(self):
        # Note that if we want innerHTML to support script execution,
        # we need to do it explicitly, like this.
        return minify('''
          model.on('innerHTML', (ev) => {
            element.innerHTML = ev.value;
            Array.from(element.querySelectorAll("script")).forEach(old=>{
              const newScript = document.createElement("script");
              Array.from(old.attributes).forEach(attr =>
                 newScript.setAttribute(attr.name, attr.value));
              newScript.appendChild(document.createTextNode(old.innerHTML));
              old.parentNode.replaceChild(newScript, old);
            });
          });
        ''')
    def widget_html(self):
        return f'''<div {self.std_attrs()}>{self.innerHTML}</div>'''

class ClickDiv(Div):
    '''
    A Div that triggers click events when anything inside them is clicked.
    If a clicked element contains a data-click value, then that value is
    sent as the click event value.
    '''
    def __init__(self, innerHTML='', **kwargs):
        super().__init__(innertHTML, **kwargs)
        self.click = Trigger()

    def widget_js(self):
        return super().widget_js() + minify('''
          element.addEventListener('click', (ev) => {
            var target = ev.target;
            while (target && target != element && !target.dataset.click) {
              target = target.parentElement;
            }
            var value = target.dataset.click;
            model.trigger('click', value);
          });
        ''')

class Image(Widget):
    """
    Just a IMG element.  Use the src property to change its contents by url,
    or use the clear() and render(imgdata) methods to convert PIL or
    tensor image data to a url to display.
    """
    def __init__(self, src='', style=None, **kwargs):
        super().__init__(style=defaulted(style, margin=0), **kwargs)
        self.src = Property(src)
        self.click = Trigger()

    def clear(self):
        """Clears the image."""
        self.src = ''

    def render(self, imgdata, source=None):
        """Converts a pil image or some tensor to a url to show inline."""
        from . import renormalize
        self.src = renormalize.as_url(imgdata, source=source)

    def widget_js(self):
        return minify('''
          model.on('src', (ev) => { element.src = ev.value; });
          element.addEventListener('click', (ev) => {
            model.trigger('click');
          });
        ''')

    def widget_html(self):
        return f'''<img {self.std_attrs()} src="{html.escape(self.src)}">'''

##########################################################################
## Utils
##########################################################################

def minify(t):
    # TODO: plug in some more real minification.
    return re.sub(r'\n\s*', '\n', t)

def style_attr(d):
    if not d:
        return ''
    return ' style="%s"' % html.escape(css_style_from_dict(d))

def data_attrs(d):
    if not d:
        return ''
    return ''.join([
        ' data-%s="%s"' % (k, html.escape(str(v))) for k, v in d.items()])

def css_style_from_dict(d):
    return ';'.join(
            re.sub('([A-Z]+)', r'-\1',k).lower() + ':' +
            re.sub('([][\\!"#$%&\'()*+,./:;<=>?@^`{|}~])', r'\\\1', str(v))
        for k, v in d.items())

def defaulted(d, **kwargs):
    if d is None:
        return kwargs
    result = dict(kwargs)
    result.update(d)
    return result

##########################################################################
## Implementation Details
##########################################################################

WIDGET_ENV = None
if WIDGET_ENV is None:
    try:
        from google.colab import output as colab_output
        WIDGET_ENV = 'colab'
    except:
        pass
if WIDGET_ENV is None:
    try:
        from ipykernel.comm import Comm as jupyter_comm
        COMM_MANAGER = get_ipython().kernel.comm_manager
        WIDGET_ENV = 'jupyter'
    except:
        pass

SEND_RECV_JS = """
function recvFromPython(obj_id, fn) {
  var recvname = "recv_" + obj_id;
  if (window[recvname] === undefined) {
    window[recvname] = new BroadcastChannel("channel_" + obj_id);
  }
  window[recvname].addEventListener("message", (ev) => {
    if (ev.data == 'ok') {
      window[recvname].ok = true;
      return;
    }
    fn.apply(null, ev.data.slice(1));
  });
}
function sendToPython(obj_id, ...args) {
  google.colab.kernel.invokeFunction('invoke_' + obj_id, args, {})
}
""" if WIDGET_ENV == 'colab' else """
function getChan(obj_id) {
  var cname = "comm_" + obj_id;
  if (!window[cname]) { window[cname] = []; }
  var chan = window[cname];
  if (!chan.comm && Jupyter.notebook.kernel) {
    chan.comm = Jupyter.notebook.kernel.comm_manager.new_comm(cname, {});
    chan.comm.on_msg((ev) => {
      if (chan.retry) { clearInterval(chan.retry); chan.retry = null; }
      if (ev.content.data == 'ok') { return; }
      var args = ev.content.data.slice(1);
      for (fn of chan) { fn.apply(null, args); }
    });
    chan.retries = 5;
    chan.retry = setInterval(() => {
      if (chan.retries) { chan.retries -= 1; chan.comm.open(); }
      else { clearInterval(chan.retry); chan.retry = null; }
    }, 2000);
  }
  return chan;
}
function recvFromPython(obj_id, fn) {
  getChan(obj_id).push(fn);
}
function sendToPython(obj_id, ...args) {
  var comm = getChan(obj_id).comm;
  if (comm) { comm.send(args); }
}
"""


WIDGET_MODEL_JS = minify(SEND_RECV_JS + """
class Model {
  constructor(obj_id, init) {
    this._id = obj_id;
    this._listeners = {};
    this._data = Object.assign({}, init)
    recvFromPython(this._id, (name, value) => {
      this._data[name] = value;
      var e = new Event(name); e.value = value;
      if (this._listeners.hasOwnProperty(name)) {
        this._listeners[name].forEach((fn) => { fn(e); });
      }
    })
  }
  trigger(name, value) {
    sendToPython(this._id, name, value);
  }
  get(name) {
    return this._data[name];
  }
  set(name, value) {
    this.trigger(name, value);
  }
  on(name, fn) {
    name.split(/\s+/).forEach((n) => {
      if (!this._listeners.hasOwnProperty(n)) {
        this._listeners[n] = [];
      }
      this._listeners[n].push(fn);
    });
  }
  off(name, fn) {
    name.split(/\s+/).forEach((n) => {
      if (!fn) {
        delete this._listeners[n];
      } else if (this._listeners.hasOwnProperty(n)) {
        this._listeners[n] = this._listeners[n].filter(
            (e) => { return e !== fn; });
      }
    });
  }
}
""")