// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CoreDescriptionModel } from './widget_core'; import { DescriptionStyleModel, DescriptionView } from './widget_description'; import { uuid } from './utils'; import { JUPYTER_CONTROLS_VERSION } from './version'; /** * Class name for a combobox with an invalid value. */ const INVALID_VALUE_CLASS = 'jpwidgets-invalidComboValue'; class StringStyleModel extends DescriptionStyleModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'StringStyleModel', _model_module: '@jupyter-widgets/controls', _model_module_version: JUPYTER_CONTROLS_VERSION, }; } public static styleProperties = { ...DescriptionStyleModel.styleProperties, background: { selector: '', attribute: 'background', default: null as any, }, font_size: { selector: '', attribute: 'font-size', default: '', }, text_color: { selector: '', attribute: 'color', default: '', }, }; } export class HTMLStyleModel extends StringStyleModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'HTMLStyleModel', _model_module: '@jupyter-widgets/controls', _model_module_version: JUPYTER_CONTROLS_VERSION, }; } public static styleProperties = { ...StringStyleModel.styleProperties, }; } export class HTMLMathStyleModel extends StringStyleModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'HTMLMathStyleModel', _model_module: '@jupyter-widgets/controls', _model_module_version: JUPYTER_CONTROLS_VERSION, }; } public static styleProperties = { ...StringStyleModel.styleProperties, }; } export class LabelStyleModel extends StringStyleModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'LabelStyleModel', _model_module: '@jupyter-widgets/controls', _model_module_version: JUPYTER_CONTROLS_VERSION, }; } public static styleProperties = { ...StringStyleModel.styleProperties, font_family: { selector: '', attribute: 'font-family', default: '', }, font_style: { selector: '', attribute: 'font-style', default: '', }, font_variant: { selector: '', attribute: 'font-variant', default: '', }, font_weight: { selector: '', attribute: 'font-weight', default: '', }, text_decoration: { selector: '', attribute: 'text-decoration', default: '', }, }; } export class TextStyleModel extends DescriptionStyleModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'TextStyleModel', _model_module: '@jupyter-widgets/controls', _model_module_version: JUPYTER_CONTROLS_VERSION, }; } public static styleProperties = { ...DescriptionStyleModel.styleProperties, background: { selector: '.widget-input', attribute: 'background', default: null as any, }, font_size: { selector: '.widget-input', attribute: 'font-size', default: '', }, text_color: { selector: '.widget-input', attribute: 'color', default: '', }, }; } export class StringModel extends CoreDescriptionModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), value: '', disabled: false, placeholder: '\u200b', _model_name: 'StringModel', }; } } export class StringView extends DescriptionView { /** * Called when view is rendered. */ render(): void { super.render(); // Incl. setting some defaults. 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. */ content: HTMLDivElement; } export class HTMLModel extends StringModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'HTMLView', _model_name: 'HTMLModel', }; } } export class HTMLView extends StringView { /** * Called when view is rendered. */ render(): void { super.render(); this.el.classList.add('widget-html'); this.content = document.createElement('div'); this.content.classList.add('widget-html-content'); this.el.appendChild(this.content); this.update(); // Set defaults. } /** * 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(): void { this.content.innerHTML = this.model.get('value'); return super.update(); } /** * Handle message sent to the front end. */ handle_message(content: any): void { if (content.do === 'focus') { this.content.focus(); } else if (content.do === 'blur') { this.content.blur(); } } content: HTMLDivElement; } export class HTMLMathModel extends StringModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'HTMLMathView', _model_name: 'HTMLMathModel', }; } } export class HTMLMathView extends StringView { /** * Called when view is rendered. */ render(): void { super.render(); this.el.classList.add('widget-htmlmath'); this.content = document.createElement('div'); this.content.classList.add('widget-htmlmath-content'); this.el.appendChild(this.content); this.update(); // Set defaults. } /** * Update the contents of this view */ update(): void { this.content.innerHTML = this.model.get('value'); this.typeset(this.content); return super.update(); } /** * Handle message sent to the front end. */ handle_message(content: any): void { if (content.do === 'focus') { this.content.focus(); } else if (content.do === 'blur') { this.content.blur(); } } content: HTMLDivElement; } export class LabelModel extends StringModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'LabelView', _model_name: 'LabelModel', }; } } export class LabelView extends StringView { /** * Called when view is rendered. */ render(): void { super.render(); this.el.classList.add('widget-label'); this.update(); // Set defaults. } /** * 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(): void { this.typeset(this.el, this.model.get('value')); return super.update(); } } export class TextareaModel extends StringModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'TextareaView', _model_name: 'TextareaModel', rows: null, continuous_update: true, }; } } export class TextareaView extends StringView { /** * Called when view is rendered. */ render(): void { super.render(); this.el.classList.add('widget-textarea'); this.textbox = document.createElement('textarea'); this.textbox.setAttribute('rows', '5'); this.textbox.id = this.label.htmlFor = uuid(); this.textbox.classList.add('widget-input'); this.el.appendChild(this.textbox); this.update(); // Set defaults. this.listenTo(this.model, 'change:placeholder', (model, value, options) => { this.update_placeholder(value); }); this.update_placeholder(); this.updateTooltip(); } update_placeholder(value?: string): void { const v = value || this.model.get('placeholder'); this.textbox.setAttribute('placeholder', v.toString()); } /** * 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?: any): void { if (options === undefined || options.updated_view !== this) { this.textbox.value = this.model.get('value'); let rows = this.model.get('rows'); if (rows === null) { rows = ''; } this.textbox.setAttribute('rows', rows); this.textbox.disabled = this.model.get('disabled'); } this.updateTabindex(); this.updateTooltip(); return super.update(); } updateTabindex(): void { if (!this.textbox) { return; // we might be constructing the parent } const tabbable = this.model.get('tabbable'); if (tabbable === true) { this.textbox.setAttribute('tabIndex', '0'); } else if (tabbable === false) { this.textbox.setAttribute('tabIndex', '-1'); } else if (tabbable === null) { this.textbox.removeAttribute('tabIndex'); } } updateTooltip(): void { if (!this.textbox) return; // we might be constructing the parent const title = this.model.get('tooltip'); if (!title) { this.textbox.removeAttribute('title'); } else if (this.model.get('description').length === 0) { this.textbox.setAttribute('title', title); } } events(): { [e: string]: string } { return { 'keydown textarea': 'handleKeyDown', 'keypress textarea': 'handleKeypress', 'input textarea': 'handleChanging', 'change textarea': 'handleChanged', }; } /** * Handle key down * * Stop propagation so the event isn't sent to the application. */ handleKeyDown(e: Event): void { e.stopPropagation(); } /** * Handles key press * * Stop propagation so the keypress isn't sent to the application. */ handleKeypress(e: Event): void { e.stopPropagation(); } /** * Triggered on input change */ handleChanging(e: Event): void { if (this.model.get('continuous_update')) { this.handleChanged(e); } } /** * Sync the value with the kernel. * * @param e Event */ handleChanged(e: Event): void { const target = e.target as HTMLTextAreaElement; this.model.set('value', target.value, { updated_view: this }); this.touch(); } /** * Handle message sent to the front end. */ handle_message(content: any): void { if (content.do === 'focus') { this.textbox.focus(); } else if (content.do === 'blur') { this.textbox.blur(); } } textbox: HTMLTextAreaElement; } export class TextModel extends StringModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'TextView', _model_name: 'TextModel', continuous_update: true, }; } } export class TextView extends StringView { /** * Called when view is rendered. */ render(): void { super.render(); this.el.classList.add('widget-text'); this.textbox = document.createElement('input'); this.textbox.setAttribute('type', this.inputType); this.textbox.id = this.label.htmlFor = uuid(); this.textbox.classList.add('widget-input'); this.el.appendChild(this.textbox); this.update(); // Set defaults. this.listenTo(this.model, 'change:placeholder', (model, value, options) => { this.update_placeholder(value); }); this.update_placeholder(); this.updateTabindex(); this.updateTooltip(); } update_placeholder(value?: string): void { this.textbox.setAttribute( 'placeholder', value || this.model.get('placeholder') ); } updateTabindex(): void { if (!this.textbox) { return; // we might be constructing the parent } const tabbable = this.model.get('tabbable'); if (tabbable === true) { this.textbox.setAttribute('tabIndex', '0'); } else if (tabbable === false) { this.textbox.setAttribute('tabIndex', '-1'); } else if (tabbable === null) { this.textbox.removeAttribute('tabIndex'); } } updateTooltip(): void { if (!this.textbox) return; // we might be constructing the parent const title = this.model.get('tooltip'); if (!title) { this.textbox.removeAttribute('title'); } else if (this.model.get('description').length === 0) { this.textbox.setAttribute('title', title); } } update(options?: any): void { /** * 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. */ if (options === undefined || options.updated_view !== this) { if (this.textbox.value !== this.model.get('value')) { this.textbox.value = this.model.get('value'); } this.textbox.disabled = this.model.get('disabled'); } return super.update(); } events(): { [e: string]: string } { return { 'keydown input': 'handleKeyDown', 'keypress input': 'handleKeypress', 'input input': 'handleChanging', 'change input': 'handleChanged', }; } /** * Handle key down * * Stop propagation so the keypress isn't sent to the application. */ handleKeyDown(e: Event): void { e.stopPropagation(); } /** * Handles text submission */ handleKeypress(e: KeyboardEvent): void { e.stopPropagation(); // The submit message is deprecated in widgets 7 if (e.keyCode === 13) { // Return key this.send({ event: 'submit' }); } } /** * Handles user input. * * Calling model.set will trigger all of the other views of the * model to update. */ handleChanging(e: Event): void { if (this.model.get('continuous_update')) { this.handleChanged(e); } } /** * Handles user input. * * Calling model.set will trigger all of the other views of the * model to update. */ handleChanged(e: Event): void { const target = e.target as HTMLInputElement; this.model.set('value', target.value, { updated_view: this }); this.touch(); } /** * Handle message sent to the front end. */ handle_message(content: any): void { if (content.do === 'focus') { this.textbox.focus(); } else if (content.do === 'blur') { this.textbox.blur(); } } protected readonly inputType: string = 'text'; textbox: HTMLInputElement; } export class PasswordModel extends TextModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _view_name: 'PasswordView', _model_name: 'PasswordModel', }; } } export class PasswordView extends TextView { protected readonly inputType: string = 'password'; } /** * Combobox widget model class. */ export class ComboboxModel extends TextModel { defaults(): Backbone.ObjectHash { return { ...super.defaults(), _model_name: 'ComboboxModel', _view_name: 'ComboboxView', options: [], ensure_options: false, }; } } /** * Combobox widget view class. */ export class ComboboxView extends TextView { render(): void { this.datalist = document.createElement('datalist'); this.datalist.id = uuid(); super.render(); this.textbox.setAttribute('list', this.datalist.id); this.el.appendChild(this.datalist); this.updateTooltip(); } update(options?: any): void { super.update(options); if (!this.datalist) { return; } const valid = this.isValid(this.model.get('value')); this.highlightValidState(valid); // Check if we need to update options if ( (options !== undefined && options.updated_view) || (!this.model.hasChanged('options') && !this.isInitialRender) ) { // Value update only, keep current options return; } this.isInitialRender = false; const opts = this.model.get('options') as string[]; const optionFragment = document.createDocumentFragment(); for (const v of opts) { const o = document.createElement('option'); o.value = v; optionFragment.appendChild(o); } this.datalist.replaceChildren(...optionFragment.children); } isValid(value: string): boolean { if (true === this.model.get('ensure_option')) { const options = this.model.get('options') as string[]; if (options.indexOf(value) === -1) { return false; } } return true; } handleChanging(e: KeyboardEvent): void { // Override to validate value const target = e.target as HTMLInputElement; const valid = this.isValid(target.value); this.highlightValidState(valid); if (valid) { super.handleChanging(e); } } handleChanged(e: KeyboardEvent): void { // Override to validate value const target = e.target as HTMLInputElement; const valid = this.isValid(target.value); this.highlightValidState(valid); if (valid) { super.handleChanged(e); } } /** * Handle message sent to the front end. */ handle_message(content: any): void { if (content.do === 'focus') { this.textbox.focus(); } else if (content.do === 'blur') { this.textbox.blur(); } } highlightValidState(valid: boolean): void { this.textbox.classList.toggle(INVALID_VALUE_CLASS, !valid); } datalist: HTMLDataListElement | undefined; isInitialRender = true; }