import { WidgetView } from '@jupyter-widgets/base' ; import { CoreDescriptionModel } from './widget_core' ; import { DescriptionView , DescriptionStyleModel } from './widget_description' ; import { uuid } from './utils' ; import * as utils from './utils' ; import noUiSlider from 'nouislider' ; export class SelectionModel extends CoreDescriptionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'SelectionModel' , index : '' , _options_labels : [ ] , disabled : false , } ; } } export class SelectionView extends DescriptionView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'jupyter-widgets' ) ; this . el . classList . add ( 'widget-inline-hbox' ) ; } /**
     * Update the contents of this view
     *
     * Called when the model is changed.  The model may have been
     * changed by another view or by a state update from the back-end.
     */ update ( ) { super . update ( ) ; if ( this . listbox ) { this . listbox . disabled = this . model . get ( 'disabled' ) ; } this . updateTabindex ( ) ; this . updateTooltip ( ) ; } updateTabindex ( ) { if ( ! this . listbox ) { return ; } const tabbable = this . model . get ( 'tabbable' ) ; if ( tabbable === true ) { this . listbox . setAttribute ( 'tabIndex' , '0' ) ; } else if ( tabbable === false ) { this . listbox . setAttribute ( 'tabIndex' , '-1' ) ; } else if ( tabbable === null ) { this . listbox . removeAttribute ( 'tabIndex' ) ; } } updateTooltip ( ) { if ( ! this . listbox ) return ; const title = this . model . get ( 'tooltip' ) ; if ( ! title ) { this . listbox . removeAttribute ( 'title' ) ; } else if ( this . model . get ( 'description' ) . length === 0 ) { this . listbox . setAttribute ( 'title' , title ) ; } } listbox : HTMLSelectElement ; } export class DropdownModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'DropdownModel' , _view_name : 'DropdownView' , button_style : '' , } ; } } export class DropdownView extends SelectionView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-dropdown' ) ; this . listbox = document . createElement ( 'select' ) ; this . listbox . id = this . label . htmlFor = uuid ( ) ; this . el . appendChild ( this . listbox ) ; this . _updateOptions ( ) ; this . update ( ) ; } /**
     * Update the contents of this view
     */ update ( options ? ) { if ( options ?. updated_view !== this ) { const optsChanged = this . model . hasChanged ( '_options_labels' ) ; if ( optsChanged ) { this . _updateOptions ( ) ; } } const index = this . model . get ( 'index' ) ; this . listbox . selectedIndex = index === null ? - 1 : index ; return super . update ( ) ; } _updateOptions ( ) { this . listbox . textContent = '' ; const items = this . model . get ( '_options_labels' ) ; for ( let i = 0 ; i < items . length ; i ++ ) { const item = items [ i ] ; const option = document . createElement ( 'option' ) ; option . textContent = item . replace ( / /g , '\xa0' ) ; option . setAttribute ( 'data-value' , encodeURIComponent ( item ) ) ; option . value = item ; this . listbox . appendChild ( option ) ; } } events ( ) { return { 'change select' : '_handle_change' , } ; } /**
     * Handle when a new value is selected.
     */ _handle_change ( ) { this . model . set ( 'index' , this . listbox . selectedIndex === - 1 ? null : this . listbox . selectedIndex , { updated_view : this } ) ; this . touch ( ) ; } /**
     * Handle message sent to the front end.
     */ handle_message ( content ) { if ( content . do === 'focus' ) { this . listbox . focus ( ) ; } else if ( content . do === 'blur' ) { this . listbox . blur ( ) ; } } } export class SelectModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'SelectModel' , _view_name : 'SelectView' , rows : 5 , } ; } } export class SelectView extends SelectionView { /**
     * Public constructor.
     */ initialize ( parameters ) { super . initialize ( parameters ) ; this . listbox = document . createElement ( 'select' ) ; } /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-select' ) ; this . listbox . id = this . label . htmlFor = uuid ( ) ; this . el . appendChild ( this . listbox ) ; this . _updateOptions ( ) ; this . update ( ) ; this . updateSelection ( ) ; } /**
     * Update the contents of this view
     */ update ( options ? ) { if ( options ?. updated_view !== this ) { const optsChange = this . model . hasChanged ( '_options_labels' ) ; const idxChange = this . model . hasChanged ( 'index' ) ; if ( optsChange || idxChange ) { const idx = this . model . get ( 'index' ) ; if ( optsChange ) { this . _updateOptions ( ) ; } this . updateSelection ( idx ) ; } } super . update ( ) ; let rows = this . model . get ( 'rows' ) ; if ( rows === null ) { rows = '' ; } this . listbox . setAttribute ( 'size' , rows ) ; } updateSelection ( index ? ) { index = index || ( this . model . get ( 'index' ) as null | number ) ; this . listbox . selectedIndex = index === null ? - 1 : index ; } _updateOptions ( ) { this . listbox . textContent = '' ; const items = this . model . get ( '_options_labels' ) ; for ( let i = 0 ; i < items . length ; i ++ ) { const item = items [ i ] ; const option = document . createElement ( 'option' ) ; option . textContent = item . replace ( / /g , '\xa0' ) ; option . setAttribute ( 'data-value' , encodeURIComponent ( item ) ) ; option . value = item ; this . listbox . appendChild ( option ) ; } } events ( ) { return { 'change select' : '_handle_change' , } ; } /**
     * Handle when a new value is selected.
     */ _handle_change ( ) { this . model . set ( 'index' , this . listbox . selectedIndex , { updated_view : this } ) ; this . touch ( ) ; } /**
     * Handle message sent to the front end.
     */ handle_message ( content ) { if ( content . do == 'focus' ) { this . listbox . focus ( ) ; } else if ( content . do == 'blur' ) { this . listbox . blur ( ) ; } } } export class RadioButtonsModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'RadioButtonsModel' , _view_name : 'RadioButtonsView' , tooltips : [ ] , icons : [ ] , button_style : '' , } ; } } export class RadioButtonsView extends DescriptionView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-radio' ) ; this . container = document . createElement ( 'div' ) ; this . el . appendChild ( this . container ) ; this . container . classList . add ( 'widget-radio-box' ) ; this . update ( ) ; } /**
     * Update the contents of this view
     *
     * Called when the model is changed.  The model may have been
     * changed by another view or by a state update from the back-end.
     */ update ( options ? ) { const items = this . model . get ( '_options_labels' ) ; const radios = Array . from ( this . container . querySelectorAll ( 'input[type="radio"]' ) ) . map ( ( x ) => x . value ) ; let stale = items . length !== radios . length ; if ( ! stale ) { for ( let i = 0 , len = items . length ; i < len ; ++ i ) { if ( radios [ i ] !== items [ i ] ) { stale = true ; break ; } } } if ( stale && ( options === undefined || options . updated_view !== this ) ) { this . container . textContent = '' ; items . forEach ( ( item , index ) => { const label = document . createElement ( 'label' ) ; label . textContent = item ; this . container . appendChild ( label ) ; const radio = document . createElement ( 'input' ) ; radio . setAttribute ( 'type' , 'radio' ) ; radio . value = index . toString ( ) ; radio . setAttribute ( 'data-value' , encodeURIComponent ( item ) ) ; label . appendChild ( radio ) ; } ) ; } items . forEach ( ( item , index ) => { const item_query = 'input[data-value="' + encodeURIComponent ( item ) + '"]' ; const radio = this . container . querySelectorAll < HTMLInputElement > ( item_query ) ; if ( radio . length > 0 ) { const radio_el = radio [ 0 ] ; radio_el . checked = this . model . get ( 'index' ) === index ; radio_el . disabled = this . model . get ( 'disabled' ) ; } } ) ; setTimeout ( this . adjustPadding , 0 , this ) ; return super . update ( options ) ; } /**
     * Adjust Padding to Multiple of Line Height
     *
     * Adjust margins so that the overall height
     * is a multiple of a single line height.
     *
     * This widget needs it because radio options
     * are spaced tighter than individual widgets
     * yet we would like the full widget line up properly
     * when displayed side-by-side with other widgets.
     */ adjustPadding ( e ) { const elStyles = window . getComputedStyle ( e . el ) ; const margins = parseInt ( elStyles . marginTop , 10 ) + parseInt ( elStyles . marginBottom , 10 ) ; const lineHeight = e . label . offsetHeight + margins ; const cStyles = window . getComputedStyle ( e . container ) ; const containerMargin = parseInt ( cStyles . marginBottom , 10 ) ; const diff = ( e . el . offsetHeight + margins - containerMargin ) % lineHeight ; const extraMargin = diff === 0 ? 0 : lineHeight - diff ; e . container . style . marginBottom = extraMargin + 'px' ; } events ( ) { return { 'click input[type="radio"]' : '_handle_click' , } ; } /**
     * Handle when a value is clicked.
     *
     * Calling model.set will trigger all of the other views of the
     * model to update.
     */ _handle_click ( event ) { const target = event . target as HTMLInputElement ; this . model . set ( 'index' , parseInt ( target . value , 10 ) , { updated_view : this } ) ; this . touch ( ) ; } /**
     * Handle message sent to the front end.
     */ handle_message ( content ) { if ( content . do == 'focus' ) { const firstItem = this . container . firstElementChild as HTMLElement ; firstItem . focus ( ) ; } else if ( content . do == 'blur' ) { for ( let i = 0 ; i < this . container . children . length ; i ++ ) { const item = this . container . children [ i ] as HTMLElement ; item . blur ( ) ; } } } container : HTMLDivElement ; } export class ToggleButtonsStyleModel extends DescriptionStyleModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'ToggleButtonsStyleModel' , } ; } public static styleProperties = { ... DescriptionStyleModel . styleProperties , button_width : { selector : '.widget-toggle-button' , attribute : 'width' , default : null as any , } , font_weight : { selector : '.widget-toggle-button' , attribute : 'font-weight' , default : '' , } , } ; } export class ToggleButtonsModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'ToggleButtonsModel' , _view_name : 'ToggleButtonsView' , } ; } } export class ToggleButtonsView extends DescriptionView { initialize ( options ) { this . _css_state = { } ; super . initialize ( options ) ; this . listenTo ( this . model , 'change:button_style' , this . update_button_style ) ; } /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-toggle-buttons' ) ; this . buttongroup = document . createElement ( 'div' ) ; this . el . appendChild ( this . buttongroup ) ; this . update ( ) ; this . set_button_style ( ) ; } /**
     * Update the contents of this view
     *
     * Called when the model is changed.  The model may have been
     * changed by another view or by a state update from the back-end.
     */ update ( options ? ) { const items = this . model . get ( '_options_labels' ) ; const icons = this . model . get ( 'icons' ) || [ ] ; const previous_icons = this . model . previous ( 'icons' ) || [ ] ; const previous_bstyle = ( ToggleButtonsView . classMap as any ) [ this . model . previous ( 'button_style' ) ] || '' ; const tooltips = this . model . get ( 'tooltips' ) || [ ] ; const disabled = this . model . get ( 'disabled' ) ; const buttons = this . buttongroup . querySelectorAll ( 'button' ) ; const values = Array . from ( buttons ) . map ( ( x ) => x . value ) ; let stale = false ; for ( let i = 0 , len = items . length ; i < len ; ++ i ) { if ( values [ i ] !== items [ i ] || icons [ i ] !== previous_icons [ i ] ) { stale = true ; break ; } } if ( stale && ( options === undefined || options . updated_view !== this ) ) { this . buttongroup . textContent = '' ; items . forEach ( ( item , index ) => { let item_html ; const empty = item . trim ( ) . length === 0 && ( ! icons [ index ] || icons [ index ] . trim ( ) . length === 0 ) ; if ( empty ) { item_html = '&nbsp;' ; } else { item_html = utils . escape_html ( item ) ; } const icon = document . createElement ( 'i' ) ; const button = document . createElement ( 'button' ) ; if ( icons [ index ] ) { icon . className = 'fa fa-' + icons [ index ] ; } button . setAttribute ( 'type' , 'button' ) ; button . className = 'widget-toggle-button jupyter-button' ; if ( previous_bstyle ) { button . classList . add ( previous_bstyle ) ; } button . innerHTML = item_html ; button . setAttribute ( 'data-value' , encodeURIComponent ( item ) ) ; button . setAttribute ( 'value' , index . toString ( ) ) ; button . appendChild ( icon ) ; button . disabled = disabled ; if ( tooltips [ index ] ) { button . setAttribute ( 'title' , tooltips [ index ] ) ; } this . update_style_traits ( button ) ; this . buttongroup . appendChild ( button ) ; } ) ; } items . forEach ( ( item , index ) => { const item_query = '[data-value="' + encodeURIComponent ( item ) + '"]' ; const button = this . buttongroup . querySelector ( item_query ) ! ; if ( this . model . get ( 'index' ) === index ) { button . classList . add ( 'mod-active' ) ; } else { button . classList . remove ( 'mod-active' ) ; } } ) ; this . stylePromise . then ( function ( style ) { if ( style ) { style . style ( ) ; } } ) ; return super . update ( options ) ; } update_style_traits ( button ? ) { for ( const name in this . _css_state as string [ ] ) { if ( Object . prototype . hasOwnProperty . call ( this . _css_state , 'name' ) ) { if ( name === 'margin' ) { this . buttongroup . style [ name ] = this . _css_state [ name ] ; } else if ( name !== 'width' ) { if ( button ) { button . style [ name ] = this . _css_state [ name ] ; } else { const buttons = this . buttongroup . querySelectorAll ( 'button' ) ; if ( buttons . length ) { buttons [ 0 ] . style [ name ] = this . _css_state [ name ] ; } } } } } } update_button_style ( ) { const buttons = this . buttongroup . querySelectorAll ( 'button' ) ; for ( let i = 0 ; i < buttons . length ; i ++ ) { this . update_mapped_classes ( ToggleButtonsView . classMap , 'button_style' , buttons [ i ] ) ; } } set_button_style ( ) { const buttons = this . buttongroup . querySelectorAll ( 'button' ) ; for ( let i = 0 ; i < buttons . length ; i ++ ) { this . set_mapped_classes ( ToggleButtonsView . classMap , 'button_style' , buttons [ i ] ) ; } } events ( ) { return { 'click button' : '_handle_click' , } ; } /**
     * Handle when a value is clicked.
     *
     * Calling model.set will trigger all of the other views of the
     * model to update.
     */ _handle_click ( event ) { const target = event . target as HTMLButtonElement ; this . model . set ( 'index' , parseInt ( target . value , 10 ) , { updated_view : this } ) ; this . touch ( ) ; this . send ( { event : 'click' } ) ; } private _css_state : any ; buttongroup : HTMLDivElement ; } export namespace ToggleButtonsView { export const classMap = { primary : [ 'mod-primary' ] , success : [ 'mod-success' ] , info : [ 'mod-info' ] , warning : [ 'mod-warning' ] , danger : [ 'mod-danger' ] , } ; } export class SelectionSliderModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'SelectionSliderModel' , _view_name : 'SelectionSliderView' , orientation : 'horizontal' , readout : true , continuous_update : true , } ; } } export class SelectionSliderView extends DescriptionView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-hslider' ) ; this . el . classList . add ( 'widget-slider' ) ; this . $slider = document . createElement ( 'div' ) ; this . $slider . classList . add ( 'slider' ) ; this . slider_container = document . createElement ( 'div' ) ; this . slider_container . classList . add ( 'slider-container' ) ; this . slider_container . appendChild ( this . $slider ) ; this . el . appendChild ( this . slider_container ) ; this . readout = document . createElement ( 'div' ) ; this . el . appendChild ( this . readout ) ; this . readout . classList . add ( 'widget-readout' ) ; this . readout . style . display = 'none' ; this . createSlider ( ) ; this . model . on ( 'change:orientation' , this . regenSlider , this ) ; this . model . on ( 'change:index' , this . updateSliderValue , this ) ; this . update ( ) ; } /**
     * Update the contents of this view
     *
     * Called when the model is changed.  The model may have been
     * changed by another view or by a state update from the back-end.
     */ update ( options ? ) { if ( options ?. updated_view !== this ) { this . updateSliderOptions ( this . model ) ; const orientation = this . model . get ( 'orientation' ) ; const disabled = this . model . get ( 'disabled' ) ; if ( disabled ) { this . readout . contentEditable = 'false' ; this . $slider . setAttribute ( 'disabled' , true ) ; } else { this . readout . contentEditable = 'true' ; this . $slider . removeAttribute ( 'disabled' ) ; } if ( orientation === 'vertical' ) { this . el . classList . remove ( 'widget-hslider' ) ; this . el . classList . remove ( 'widget-inline-hbox' ) ; this . el . classList . add ( 'widget-vslider' ) ; this . el . classList . add ( 'widget-inline-vbox' ) ; } else { this . el . classList . remove ( 'widget-vslider' ) ; this . el . classList . remove ( 'widget-inline-vbox' ) ; this . el . classList . add ( 'widget-hslider' ) ; this . el . classList . add ( 'widget-inline-hbox' ) ; } const readout = this . model . get ( 'readout' ) ; if ( readout ) { this . readout . style . display = '' ; } else { this . readout . style . display = 'none' ; } this . updateSelection ( ) ; } return super . update ( options ) ; } regenSlider ( e ) { this . $slider . noUiSlider . destroy ( ) ; this . createSlider ( ) ; } createSlider ( ) { const labels = this . model . get ( '_options_labels' ) ; const min = 0 ; const max = labels . length - 1 ; const orientation = this . model . get ( 'orientation' ) ; const behavior = this . model . get ( 'behavior' ) ; noUiSlider . create ( this . $slider , { start : this . model . get ( 'index' ) , connect : true , behaviour : behavior , range : { min : min , max : max , } , step : 1 , animate : false , orientation : orientation , direction : orientation === 'horizontal' ? 'ltr' : 'rtl' , format : { from : ( value ) : number => Number ( value ) , to : ( value ) : number => value , } , } ) ; this . $slider . noUiSlider . on ( 'update' , ( values , handle ) => { this . handleSliderChange ( values , handle ) ; } ) ; this . $slider . noUiSlider . on ( 'end' , ( values , handle ) => { this . handleSliderChanged ( values , handle ) ; } ) ; } events ( ) { return { slide : 'handleSliderChange' , slidestop : 'handleSliderChanged' , } ; } updateSelection ( ) { const index = this . model . get ( 'index' ) ; this . updateReadout ( index ) ; } updateReadout ( index ) { const value = this . model . get ( '_options_labels' ) [ index ] ; this . readout . textContent = value ; } /**
     * Called when the slider value is changing.
     */ handleSliderChange ( values , handle ) { const index = values [ 0 ] ; this . updateReadout ( index ) ; if ( this . model . get ( 'continuous_update' ) ) { this . handleSliderChanged ( values , handle ) ; } } /**
     * Called when the slider value has changed.
     *
     * Calling model.set will trigger all of the other views of the
     * model to update.
     */ handleSliderChanged ( values , handle ) { const index = values [ 0 ] ; this . updateReadout ( index ) ; this . model . set ( 'index' , index , { updated_view : this } ) ; this . touch ( ) ; } updateSliderOptions ( e ) { const labels = this . model . get ( '_options_labels' ) ; const min = 0 ; const max = labels . length - 1 ; this . $slider . noUiSlider . updateOptions ( { start : this . model . get ( 'index' ) , range : { min : min , max : max , } , step : 1 , } ) ; } updateSliderValue ( model , _ , options ) { if ( options . updated_view === this ) { return ; } const prev_index = this . $slider . noUiSlider . get ( ) ; const index = this . model . get ( 'index' ) ; if ( prev_index !== index ) { this . $slider . noUiSlider . set ( index ) ; } } $slider : any ; slider_container : HTMLDivElement ; readout : HTMLDivElement ; } export class MultipleSelectionModel extends SelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'MultipleSelectionModel' } ; } } export class SelectMultipleModel extends MultipleSelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'SelectMultipleModel' , _view_name : 'SelectMultipleView' , rows : null , } ; } } export class SelectMultipleView extends SelectView { /**
     * Public constructor.
     */ initialize ( parameters ) { super . initialize ( parameters ) ; this . listbox . multiple = true ; } /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'widget-select-multiple' ) ; } updateSelection ( ) { const selected = this . model . get ( 'index' ) || [ ] ; const listboxOptions = this . listbox . options ; this . listbox . selectedIndex = - 1 ; selected . forEach ( ( i ) => { listboxOptions [ i ] . selected = true ; } ) ; } /**
     * Handle when a new value is selected.
     */ _handle_change ( ) { const index = Array . prototype . map . call ( this . listbox . selectedOptions || [ ] , function ( option ) { return option . index ; } ) ; this . model . set ( 'index' , index , { updated_view : this } ) ; this . touch ( ) ; } } export class SelectionRangeSliderModel extends MultipleSelectionModel { defaults ( ) { return { ... super . defaults ( ) , _model_name : 'SelectionSliderModel' , _view_name : 'SelectionSliderView' , orientation : 'horizontal' , readout : true , continuous_update : true , } ; } } export class SelectionRangeSliderView extends SelectionSliderView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; } updateSelection ( index ? ) { index = index || ( this . model . get ( 'index' ) as number [ ] ) ; this . updateReadout ( index ) ; } updateReadout ( index ) { const labels = this . model . get ( '_options_labels' ) ; const minValue = labels [ index [ 0 ] ] ; const maxValue = labels [ index [ 1 ] ] ; this . readout . textContent = `${ minValue }-${ maxValue }` ; } /**
     * Called when the slider value is changing.
     */ handleSliderChange ( values , handle ) { const intValues = values . map ( Math . trunc ) ; this . updateReadout ( intValues ) ; if ( this . model . get ( 'continuous_update' ) ) { this . handleSliderChanged ( values , handle ) ; } } /**
     * Called when the slider value has changed.
     *
     * Calling model.set will trigger all of the other views of the
     * model to update.
     */ handleSliderChanged ( values , handle ) { const intValues = values . map ( Math . round ) ; this . updateReadout ( intValues ) ; this . model . set ( 'index' , intValues . slice ( ) , { updated_view : this } ) ; this . touch ( ) ; } updateSliderValue ( model , _ , options ) { if ( options . updated_view === this ) { return ; } const prev_index = this . $slider . noUiSlider . get ( ) . map ( Math . round ) ; const index = this . model . get ( 'index' ) . map ( Math . round ) ; if ( prev_index [ 0 ] !== index [ 0 ] || prev_index [ 1 ] !== index [ 1 ] ) { this . $slider . noUiSlider . set ( index ) ; } } $slider : any ; slider_container : HTMLDivElement ; readout : HTMLDivElement ; }