import * as d3Color from 'd3-color' ; import * as d3Format from 'd3-format' ; import { CoreDOMWidgetModel } from './widget_core' ; import { DOMWidgetView , Dict , uuid } from '@jupyter-widgets/base' ; /**
 * Returns a new string after removing any leading and trailing whitespaces.
 * The original string is left unchanged.
 */ function trim ( value ) { return value . replace ( /^\s+|\s+$/g , '' ) ; } /**
 * Clamp a number between min and max and return the result.
 */ function clamp ( value , min , max ) { return Math . min ( Math . max ( value , min ) , max ) ; } /**
 * Remove children from an HTMLElement
 */ function removeChildren ( el ) { while ( el . firstChild ) { el . removeChild ( el . firstChild ) ; } } /**
 * Selection class which keeps track on selected indices.
 */ class Selection { constructor ( start , dx , max ) { this . start = start ; this . dx = dx ; this . max = max ; } /**
     * Check if a given index is currently selected.
     */ isSelected ( index ) { let min ; let max ; if ( this . dx >= 0 ) { min = this . start ; max = this . start + this . dx ; } else { min = this . start + this . dx ; max = this . start ; } return min <= index && index < max ; } /**
     * Update selection
     */ updateSelection ( dx ) { this . dx += dx ; if ( this . start + this . dx > this . max ) { this . dx = this . max - this . start ; } if ( this . start + this . dx < 0 ) { this . dx = - this . start ; } } private start : number ; private dx : number ; private max : number ; } class TagsInputBaseModel extends CoreDOMWidgetModel { defaults ( ) { return { ... super . defaults ( ) , value : [ ] , placeholder : '\u200b' , allowed_tags : null , allow_duplicates : true , } ; } } abstract class TagsInputBaseView extends DOMWidgetView { /**
     * Called when view is rendered.
     */ render ( ) { super . render ( ) ; this . el . classList . add ( 'jupyter-widgets' ) ; this . el . classList . add ( 'jupyter-widget-tagsinput' ) ; this . taginputWrapper = document . createElement ( 'div' ) ; if ( this . model . get ( 'value' ) . length ) { this . taginputWrapper . style . display = 'none' ; } else { this . taginputWrapper . style . display = 'inline-block' ; } this . datalistID = uuid ( ) ; this . taginput = document . createElement ( 'input' ) ; this . taginput . classList . add ( 'jupyter-widget-tag' ) ; this . taginput . classList . add ( 'jupyter-widget-taginput' ) ; this . taginput . setAttribute ( 'list' , this . datalistID ) ; this . taginput . setAttribute ( 'type' , 'text' ) ; this . autocompleteList = document . createElement ( 'datalist' ) ; this . autocompleteList . id = this . datalistID ; this . updateAutocomplete ( ) ; this . model . on ( 'change:allowed_tags' , this . updateAutocomplete . bind ( this ) ) ; this . updatePlaceholder ( ) ; this . model . on ( 'change:placeholder' , this . updatePlaceholder . bind ( this ) ) ; this . taginputWrapper . classList . add ( 'widget-text' ) ; this . taginputWrapper . appendChild ( this . taginput ) ; this . taginputWrapper . appendChild ( this . autocompleteList ) ; this . el . onclick = this . focus . bind ( this ) ; this . el . ondrop = ( event ) : void => { const index = this . hoveredTagIndex == null ? this . tags . length : this . hoveredTagIndex ; return this . ondrop ( event , index ) ; } ; this . el . ondragover = this . ondragover . bind ( this ) ; this . taginput . onchange = this . handleValueAdded . bind ( this ) ; this . taginput . oninput = this . resizeInput . bind ( this ) ; this . taginput . onkeydown = this . handleKeyEvent . bind ( this ) ; this . taginput . onblur = this . loseFocus . bind ( this ) ; this . resizeInput ( ) ; this . inputIndex = this . model . get ( 'value' ) . length ; this . selection = null ; this . preventLoosingFocus = false ; 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 ( ) { this . preventLoosingFocus = true ; removeChildren ( this . el ) ; this . tags = [ ] ; const value = this . model . get ( 'value' ) ; for ( const idx in value ) { const index = parseInt ( idx ) ; const tag = this . createTag ( value [ index ] , index , this . selection != null && this . selection . isSelected ( index ) ) ; tag . draggable = true ; tag . ondragstart = ( ( index , value ) => { return ( event ) : void => { this . ondragstart ( event , index , value , this . model . model_id ) ; } ; } ) ( index , value [ index ] ) ; tag . ondrop = ( ( index ) => { return ( event ) : void => { this . ondrop ( event , index ) ; } ; } ) ( index ) ; tag . ondragover = this . ondragover . bind ( this ) ; tag . ondragenter = ( ( index ) => { return ( event ) : void => { this . ondragenter ( event , index ) ; } ; } ) ( index ) ; tag . ondragend = this . ondragend . bind ( this ) ; this . tags . push ( tag ) ; this . el . appendChild ( tag ) ; } this . el . insertBefore ( this . taginputWrapper , this . el . children [ this . inputIndex ] ) ; if ( this . model . get ( 'value' ) . length ) { this . taginputWrapper . style . display = 'none' ; } else { this . taginputWrapper . style . display = 'inline-block' ; } this . preventLoosingFocus = false ; return super . update ( ) ; } /**
     * Update the auto-completion list
     */ updateAutocomplete ( ) { removeChildren ( this . autocompleteList ) ; const allowedTags = this . model . get ( 'allowed_tags' ) ; for ( const tag of allowedTags ) { const option = document . createElement ( 'option' ) ; option . value = tag ; this . autocompleteList . appendChild ( option ) ; } } /**
     * Update the auto-completion list
     */ updatePlaceholder ( ) { this . taginput . placeholder = this . model . get ( 'placeholder' ) ; this . resizeInput ( ) ; } /**
     * Update the tags, called when the selection has changed and we need to update the tags CSS
     */ updateTags ( ) { const value = this . model . get ( 'value' ) ; for ( const idx in this . tags ) { const index = parseInt ( idx ) ; this . updateTag ( this . tags [ index ] , value [ index ] , index , this . selection != null && this . selection . isSelected ( index ) ) ; } } /**
     * Handle a new value is added from the input element
     */ handleValueAdded ( event ) { const newTagValue = trim ( this . taginput . value ) ; const tagIndex = this . inputIndex ; if ( newTagValue == '' ) { return ; } this . inputIndex ++ ; const tagAdded = this . addTag ( tagIndex , newTagValue ) ; if ( tagAdded ) { this . taginput . value = '' ; this . resizeInput ( ) ; this . focus ( ) ; } } /**
     * Add a new tag with a value of `tagValue` at the `index` position
     * Return true if the tag was correctly added, false otherwise
     */ addTag ( index , tagValue ) { const value = this . model . get ( 'value' ) ; let newTagValue ; try { newTagValue = this . validateValue ( tagValue ) ; } catch ( error ) { return false ; } const allowedTagValues = this . model . get ( 'allowed_tags' ) ; if ( allowedTagValues . length && ! allowedTagValues . includes ( newTagValue ) ) { return false ; } if ( ! this . model . get ( 'allow_duplicates' ) && value . includes ( newTagValue ) ) { return false ; } this . selection = null ; const newValue = [ ... value ] ; newValue . splice ( index , 0 , newTagValue ) ; this . model . set ( 'value' , newValue ) ; this . model . save_changes ( ) ; return true ; } /**
     * Resize the input element
     */ resizeInput ( ) { let content ; if ( this . taginput . value . length != 0 ) { content = this . taginput . value ; } else { content = this . model . get ( 'placeholder' ) ; } const size = content . length + 1 ; this . taginput . setAttribute ( 'size' , String ( size ) ) ; } /**
     * Handle key events on the input element
     */ handleKeyEvent ( event ) { const valueLength = this . model . get ( 'value' ) . length ; if ( this . taginput . value . length ) { return ; } const currentElement = this . inputIndex ; switch ( event . key ) { case 'ArrowLeft' : if ( event . ctrlKey && event . shiftKey ) { this . select ( currentElement , - currentElement ) ; } if ( ! event . ctrlKey && event . shiftKey ) { this . select ( currentElement , - 1 ) ; } if ( event . ctrlKey ) { this . inputIndex = 0 ; } else { this . inputIndex -- ; } break ; case 'ArrowRight' : if ( event . ctrlKey && event . shiftKey ) { this . select ( currentElement , valueLength - currentElement ) ; } if ( ! event . ctrlKey && event . shiftKey ) { this . select ( currentElement , 1 ) ; } if ( event . ctrlKey ) { this . inputIndex = valueLength ; } else { this . inputIndex ++ ; } break ; case 'Backspace' : if ( this . selection ) { this . removeSelectedTags ( ) ; } else { this . removeTag ( this . inputIndex - 1 ) ; } break ; case 'Delete' : if ( this . selection ) { this . removeSelectedTags ( ) ; } else { this . removeTag ( this . inputIndex ) ; } break ; default : return ; break ; } if ( ! event . shiftKey ) { this . selection = null ; } this . inputIndex = clamp ( this . inputIndex , 0 , valueLength ) ; this . update ( ) ; this . focus ( ) ; } /**
     * Function that gets called when a tag with a given `value` is being dragged.
     */ ondragstart ( event , index , tagValue , origin ) { if ( event . dataTransfer == null ) { return ; } event . dataTransfer . setData ( 'index' , String ( index ) ) ; event . dataTransfer . setData ( 'tagValue' , String ( tagValue ) ) ; event . dataTransfer . setData ( 'origin' , origin ) ; } /**
     * Function that gets called when a tag has been dragged on the tag at the `index` position.
     */ ondrop ( event , index ) { if ( event . dataTransfer == null ) { return ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; const draggedTagValue = event . dataTransfer . getData ( 'tagValue' ) ; const draggedTagindex = parseInt ( event . dataTransfer . getData ( 'index' ) ) ; const sameOrigin = event . dataTransfer . getData ( 'origin' ) == this . model . model_id ; if ( isNaN ( draggedTagindex ) ) { return ; } if ( sameOrigin ) { const value = this . model . get ( 'value' ) ; const newValue = [ ... value ] ; if ( draggedTagindex < index ) { index -- ; } newValue . splice ( draggedTagindex , 1 ) ; newValue . splice ( index , 0 , draggedTagValue ) ; this . model . set ( 'value' , newValue ) ; this . model . save_changes ( ) ; return ; } this . addTag ( index , draggedTagValue ) ; } ondragover ( event ) { event . preventDefault ( ) ; } ondragenter ( event , index ) { if ( this . hoveredTag != null && this . hoveredTag != this . tags [ index ] ) { this . hoveredTag . style . marginLeft = '1px' ; } this . hoveredTag = this . tags [ index ] ; this . hoveredTagIndex = index ; this . hoveredTag . style . marginLeft = '30px' ; } ondragend ( ) { if ( this . hoveredTag != null ) { this . hoveredTag . style . marginLeft = '1px' ; } this . hoveredTag = null ; this . hoveredTagIndex = null ; } /**
     * Select tags from `start` to `start + dx` not included.
     */ select ( start , dx ) { const valueLength = this . model . get ( 'value' ) . length ; if ( ! this . selection ) { this . selection = new Selection ( start , dx , valueLength ) ; } else { this . selection . updateSelection ( dx ) ; } } /**
     * Remove all the selected tags.
     */ removeSelectedTags ( ) { const value = [ ... this . model . get ( 'value' ) ] ; const valueLength = value . length ; for ( let idx = valueLength - 1 ; idx >= 0 ; idx -- ) { if ( this . selection != null && this . selection . isSelected ( idx ) ) { value . splice ( idx , 1 ) ; if ( idx < this . inputIndex ) { this . inputIndex -- ; } } } this . model . set ( 'value' , value ) ; this . model . save_changes ( ) ; } /**
     * Remove a tag given its index in the list
     */ removeTag ( tagIndex ) { const value = [ ... this . model . get ( 'value' ) ] ; value . splice ( tagIndex , 1 ) ; if ( tagIndex < this . inputIndex ) { this . inputIndex -- ; } this . model . set ( 'value' , value ) ; this . model . save_changes ( ) ; } /**
     * Focus on the input element
     */ focus ( ) { this . taginputWrapper . style . display = 'inline-block' ; this . taginput . focus ( ) ; } /**
     * Lose focus on the input element
     */ loseFocus ( ) { if ( this . preventLoosingFocus ) { return ; } if ( this . model . get ( 'value' ) . length ) { this . taginputWrapper . style . display = 'none' ; } this . selection = null ; this . updateTags ( ) ; } preinitialize ( ) { this . tagName = 'div' ; } /**
     * Validate an input tag typed by the user, returning the correct tag type. This should be overridden in subclasses.
     */ validateValue ( value ) { return value ; } abstract createTag ( value , index , selected ) ; abstract updateTag ( tag , value , index , selected ) ; el : HTMLDivElement ; taginputWrapper : HTMLDivElement ; taginput : HTMLInputElement ; autocompleteList : HTMLDataListElement ; tags : HTMLElement [ ] ; hoveredTag : HTMLElement | null = null ; hoveredTagIndex : number | null = null ; datalistID : string ; inputIndex : number ; selection : null | Selection ; preventLoosingFocus : boolean ; model : TagsInputBaseModel ; } export class TagsInputModel extends TagsInputBaseModel { defaults ( ) { return { ... super . defaults ( ) , value : [ ] , tag_style : '' , _view_name : 'TagsInputView' , _model_name : 'TagsInputModel' , } ; } } export class TagsInputView extends TagsInputBaseView { /**
     * Create the string tag
     */ createTag ( value , index , selected ) { const tag = document . createElement ( 'div' ) ; const style = this . model . get ( 'tag_style' ) ; tag . classList . add ( 'jupyter-widget-tag' ) ; tag . classList . add ( TagsInputView . class_map [ style ] ) ; if ( selected ) { tag . classList . add ( 'mod-active' ) ; } tag . appendChild ( document . createTextNode ( this . getTagText ( value ) ) ) ; const i = document . createElement ( 'i' ) ; i . classList . add ( 'fa' ) ; i . classList . add ( 'fa-times' ) ; i . classList . add ( 'jupyter-widget-tag-close' ) ; tag . appendChild ( i ) ; i . onmousedown = ( ( index ) => { return ( ) : void => { this . removeTag ( index ) ; this . loseFocus ( ) ; } ; } ) ( index ) ; return tag ; } /**
     * Returns the text that should be displayed in the tag element
     */ getTagText ( value ) { return value ; } /**
     * Update a given tag
     */ updateTag ( tag , value , index , selected ) { if ( selected ) { tag . classList . add ( 'mod-active' ) ; } else { tag . classList . remove ( 'mod-active' ) ; } } model : TagsInputModel ; static class_map : Dict < string > = { primary : 'mod-primary' , success : 'mod-success' , info : 'mod-info' , warning : 'mod-warning' , danger : 'mod-danger' , } ; } export class ColorsInputModel extends TagsInputBaseModel { defaults ( ) { return { ... super . defaults ( ) , value : [ ] , _view_name : 'ColorsInputView' , _model_name : 'ColorsInputModel' , } ; } } export class ColorsInputView extends TagsInputBaseView { /**
     * Create the Color tag
     */ createTag ( value , index , selected ) { const tag = document . createElement ( 'div' ) ; const color = value ; const darkerColor = d3Color . color ( value ) ! . darker ( ) . toString ( ) ; tag . classList . add ( 'jupyter-widget-tag' ) ; tag . classList . add ( 'jupyter-widget-colortag' ) ; if ( ! selected ) { tag . style . backgroundColor = color ; } else { tag . classList . add ( 'mod-active' ) ; tag . style . backgroundColor = darkerColor ; } const i = document . createElement ( 'i' ) ; i . classList . add ( 'fa' ) ; i . classList . add ( 'fa-times' ) ; i . classList . add ( 'jupyter-widget-tag-close' ) ; tag . appendChild ( i ) ; i . onmousedown = ( ( index ) => { return ( ) : void => { this . removeTag ( index ) ; this . loseFocus ( ) ; } ; } ) ( index ) ; return tag ; } /**
     * Update a given tag
     */ updateTag ( tag , value , index , selected ) { const color = value ; const darkerColor = d3Color . color ( value ) ! . darker ( ) . toString ( ) ; if ( ! selected ) { tag . classList . remove ( 'mod-active' ) ; tag . style . backgroundColor = color ; } else { tag . classList . add ( 'mod-active' ) ; tag . style . backgroundColor = darkerColor ; } } /**
     * Validate an input tag typed by the user, returning the correct tag type. This should be overridden in subclasses.
     */ validateValue ( value ) { if ( d3Color . color ( value ) == null ) { throw value + ' is not a valid Color' ; } return value ; } model : ColorsInputModel ; } abstract class NumbersInputModel extends TagsInputModel { defaults ( ) { return { ... super . defaults ( ) , min : null , max : null , } ; } } abstract class NumbersInputView extends TagsInputView { render ( ) { this . model . on ( 'change:format' , ( ) => { this . formatter = d3Format . format ( this . model . get ( 'format' ) ) ; this . update ( ) ; } ) ; this . formatter = d3Format . format ( this . model . get ( 'format' ) ) ; super . render ( ) ; } /**
     * Returns the text that should be displayed in the tag element
     */ getTagText ( value ) { return this . formatter ( this . parseNumber ( value ) ) ; } /**
     * Validate an input tag typed by the user, returning the correct tag type. This should be overridden in subclasses.
     */ validateValue ( value ) { const parsed = this . parseNumber ( value ) ; const min = this . model . get ( 'min' ) ; const max = this . model . get ( 'max' ) ; if ( isNaN ( parsed ) || ( min != null && parsed < min ) || ( max != null && parsed > max ) ) { throw ( value + ' is not a valid number, it should be in the range [' + min + ', ' + max + ']' ) ; } return parsed ; } abstract parseNumber ( value ) ; formatter : ( value : number ) => string ; } export class FloatsInputModel extends NumbersInputModel { defaults ( ) { return { ... super . defaults ( ) , _view_name : 'FloatsInputView' , _model_name : 'FloatsInputModel' , format : '.1f' , } ; } } export class FloatsInputView extends NumbersInputView { parseNumber ( value ) { return parseFloat ( value ) ; } model : FloatsInputModel ; } export class IntsInputModel extends NumbersInputModel { defaults ( ) { return { ... super . defaults ( ) , _view_name : 'IntsInputView' , _model_name : 'IntsInputModel' , format : '.3g' , } ; } } export class IntsInputView extends NumbersInputView { parseNumber ( value ) { const int = parseInt ( value ) ; if ( int != parseFloat ( value ) ) { throw value + ' should be an integer' ; } return int ; } model : IntsInputModel ; }