UNPKG

@gitlab/ui

Version:
543 lines (532 loc) • 16.9 kB
import { toString, uniqueId, isBoolean, toInteger } from 'lodash-es'; import { toFloat } from '../../../../utils/number_utils'; import { stopEvent, isVisible } from '../../../../utils/utils'; import { VBVisible } from '../../../../vendor/bootstrap-vue/src/directives/visible/visible'; import GlFormCharacterCount from '../form_character_count/form_character_count'; import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js'; var script = { name: 'GlFormTextarea', components: { GlFormCharacterCount }, directives: { 'b-visible': VBVisible }, inheritAttrs: false, model: { prop: 'value', event: 'input' }, props: { /** * The current value of the textarea. */ value: { type: String, required: false, default: '' }, /** * When true, prevents the textarea from being resized by the user (hides the resize handle). */ noResize: { type: Boolean, required: false, default: true }, /** * When true, emits a submit event when Ctrl+Enter or Cmd+Enter is pressed. */ submitOnEnter: { type: Boolean, required: false, default: false }, /** * Max character count for the textarea. */ characterCountLimit: { type: Number, required: false, default: null }, /** * Additional CSS class(es) to apply to the textarea element. */ textareaClasses: { type: [String, Object, Array], required: false, default: null }, /** * Number of visible text rows in the textarea. */ rows: { type: [Number, String], required: false, default: 4 }, /** * Used to set the `id` attribute on the rendered content. */ id: { type: String, required: false, default: undefined }, /** * When set to `true`, attempts to auto-focus the control when it is mounted. */ autofocus: { type: Boolean, required: false, default: false }, /** * When set to `true`, disables the component's functionality. */ disabled: { type: Boolean, required: false, default: false }, /** * ID of the form that the form control belongs to. Sets the `form` attribute on the control. */ form: { type: String, required: false, default: undefined }, /** * Sets the value of the `name` attribute on the form control. */ name: { type: String, required: false, default: undefined }, /** * Adds the `required` attribute to the form control. */ required: { type: Boolean, required: false, default: false }, /** * Controls the validation state appearance of the component. * `true` for valid, `false` for invalid, or `null` for no validation state. */ state: { type: Boolean, required: false, default: null }, /** * Optional value to set for the 'aria-invalid' attribute. */ ariaInvalid: { type: [Boolean, String], required: false, default: false }, /** * Sets the 'autocomplete' attribute value on the form control. */ autocomplete: { type: String, required: false, default: undefined }, /** * When set to a number of milliseconds greater than zero, will debounce the user input. */ debounce: { type: [Number, String], required: false, default: 0 }, /** * Reference to a function for formatting the input. */ formatter: { type: Function, required: false, default: undefined }, /** * Sets the `placeholder` attribute value on the form control. */ placeholder: { type: String, required: false, default: undefined }, /** * Sets the `readonly` attribute on the form control. */ readonly: { type: Boolean, required: false, default: false }, /** * Set the size of the component's appearance. 'sm' or 'lg'. Defaults to medium size when omitted. */ size: { type: String, required: false, default: undefined }, /** * The maximum number of rows to show. When set, enables auto-height. */ maxRows: { type: [Number, String], required: false, default: undefined } }, data() { return { localValue: toString(this.value), vModelValue: this.value, localId: null, heightInPx: null, characterCountTextId: uniqueId('form-textarea-character-count-') }; }, computed: { computedId() { return this.id || this.localId; }, computedState() { return isBoolean(this.state) ? this.state : null; }, stateClass() { if (this.computedState === true) return 'is-valid'; if (this.computedState === false) return 'is-invalid'; return null; }, computedAriaInvalid() { const ariaInvalid = this.ariaInvalid; if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') { return 'true'; } return this.computedState === false ? 'true' : ariaInvalid; }, sizeClass() { return this.size ? `form-control-${this.size}` : null; }, computedDebounce() { return Math.max(toInteger(this.debounce), 0); }, hasFormatter() { return typeof this.formatter === 'function'; }, computedMinRows() { // Ensure rows is at least 2 and positive (2 is the native textarea value) // A value of 1 can cause issues in some browsers, and most browsers // only support 2 as the smallest value return Math.max(toInteger(this.rows), 2); }, computedMaxRows() { return Math.max(this.computedMinRows, toInteger(this.maxRows)); }, computedRows() { // This is used to set the attribute 'rows' on the textarea // If auto-height is enabled, then we return `null` as we use CSS to control height return this.computedMinRows === this.computedMaxRows ? this.computedMinRows : null; }, computedStyle() { const styles = { // Setting `noResize` to true will disable the ability for the user to // manually resize the textarea. We also disable when in auto height mode resize: !this.computedRows || this.noResize ? 'none' : null }; if (!this.computedRows) { // Conditionally set the computed CSS height when auto rows/height is enabled // We avoid setting the style to `null`, which can override user manual resize handle styles.height = this.heightInPx; // We always add a vertical scrollbar to the textarea when auto-height is // enabled so that the computed height calculation returns a stable value styles.overflowY = 'scroll'; } return styles; }, computedAttrs() { const disabled = this.disabled, required = this.required, readonly = this.readonly; return { ...this.$attrs, id: this.computedId, name: this.name || null, form: this.form || null, disabled, placeholder: this.placeholder || null, required, autocomplete: this.autocomplete || null, readonly, rows: this.computedRows, 'aria-required': required ? 'true' : null, 'aria-invalid': this.computedAriaInvalid }; }, computedClass() { return ['gl-form-input', 'gl-form-textarea', 'form-control', this.textareaClasses, this.sizeClass, this.stateClass]; }, computedListeners() { return { ...this.$listeners, input: this.onInput, change: this.onChange, blur: this.onBlur }; }, keypressEvent() { return this.submitOnEnter ? 'keyup' : null; }, showCharacterCount() { return this.characterCountLimit !== null; } }, watch: { value(newValue) { const stringifyValue = toString(newValue); if (stringifyValue !== this.localValue || newValue !== this.vModelValue) { this.clearDebounce(); this.localValue = stringifyValue; this.vModelValue = newValue; } }, localValue() { this.setHeight(); } }, created() { this.$_inputDebounceTimer = null; }, mounted() { this.handleAutofocus(); this.setHeight(); this.$nextTick(() => { this.localId = uniqueId('gl-form-textarea-'); }); }, beforeDestroy() { this.clearDebounce(); }, methods: { focus() { if (!this.disabled) { var _this$$refs$input; (_this$$refs$input = this.$refs.input) === null || _this$$refs$input === void 0 ? void 0 : _this$$refs$input.focus(); } }, blur() { if (!this.disabled) { var _this$$refs$input2; (_this$$refs$input2 = this.$refs.input) === null || _this$$refs$input2 === void 0 ? void 0 : _this$$refs$input2.blur(); } }, clearDebounce() { clearTimeout(this.$_inputDebounceTimer); this.$_inputDebounceTimer = null; }, formatValue(value, event) { let newValue = toString(value); if (this.hasFormatter) { newValue = this.formatter(newValue, event); } return newValue; }, updateValue(value) { let force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; this.clearDebounce(); const doUpdate = () => { if (value !== this.vModelValue) { this.vModelValue = value; /** * Triggered by user interaction. * Emitted after any formatting (not including 'trim' or 'number' props). * Useful for getting the currently entered value when the 'debounce'is set. * * @event input */ this.$emit('input', value); } else if (this.hasFormatter) { const input = this.$refs.input; if (input && value !== input.value) { input.value = value; } } }; if (this.computedDebounce > 0 && !force) { this.$_inputDebounceTimer = setTimeout(doUpdate, this.computedDebounce); } else { doUpdate(); } }, onInput(event) { const value = event.target.value; const formattedValue = this.formatValue(value, event); if (formattedValue === false || event.defaultPrevented) { stopEvent(event, { propagation: false }); return; } this.localValue = formattedValue; this.updateValue(formattedValue); /** * The `input` and `update` events are swapped * see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1628. * * @event update */ this.$emit('update', formattedValue); }, onChange(event) { const value = event.target.value; const formattedValue = this.formatValue(value, event); if (formattedValue === false || event.defaultPrevented) { stopEvent(event, { propagation: false }); return; } this.localValue = formattedValue; this.updateValue(formattedValue, true); /** * Change event triggered by user interaction. * Emitted after any formatting (not including 'trim' or 'number' props) * and after the v-model is updated. The `input` and `update` events are swapped * see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1628. * * @event change */ this.$emit('change', formattedValue); }, onBlur(event) { const value = event.target.value; const formattedValue = this.formatValue(value, event); if (formattedValue !== false) { this.localValue = toString(formattedValue); this.updateValue(formattedValue, true); } /** * Emitted after the textarea loses focus * * @event blur */ this.$emit('blur', event); }, handleAutofocus() { this.$nextTick(() => { window.requestAnimationFrame(() => { if (this.autofocus && isVisible(this.$refs.input)) { this.focus(); } }); }); }, handleKeyPress(e) { if (this.submitOnEnter && e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { /** * Emitted after enter is pressed in textarea * * @event submit */ this.$emit('submit'); } }, visibleCallback(visible) { if (visible) { this.$nextTick(this.setHeight); } }, setHeight() { this.$nextTick(() => { window.requestAnimationFrame(() => { this.heightInPx = this.computeHeight(); }); }); }, computeHeight() { if (this.computedRows !== null) { return null; } const el = this.$refs.input; if (!el || !isVisible(el)) { return null; } const computedStyle = getComputedStyle(el); const lineHeight = toFloat(computedStyle.lineHeight, 1); const border = toFloat(computedStyle.borderTopWidth, 0) + toFloat(computedStyle.borderBottomWidth, 0); const padding = toFloat(computedStyle.paddingTop, 0) + toFloat(computedStyle.paddingBottom, 0); const offset = border + padding; const minHeight = lineHeight * this.computedMinRows + offset; const oldHeight = el.style.height || computedStyle.height; el.style.height = 'auto'; const scrollHeight = el.scrollHeight; el.style.height = oldHeight; const contentRows = Math.max((scrollHeight - padding) / lineHeight, 2); const rows = Math.min(Math.max(contentRows, this.computedMinRows), this.computedMaxRows); const height = Math.max(Math.ceil(rows * lineHeight + offset), minHeight); return `${height}px`; }, select() { this.$refs.input.select(...arguments); }, setSelectionRange() { this.$refs.input.setSelectionRange(...arguments); }, setRangeText() { this.$refs.input.setRangeText(...arguments); }, setCustomValidity() { return this.$refs.input.setCustomValidity(...arguments); }, checkValidity() { return this.$refs.input.checkValidity(...arguments); }, reportValidity() { return this.$refs.input.reportValidity(...arguments); } } }; /* script */ const __vue_script__ = script; /* template */ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.showCharacterCount)?_c('div',[_c('textarea',_vm._g(_vm._b({directives:[{name:"b-visible",rawName:"v-b-visible.640",value:(_vm.visibleCallback),expression:"visibleCallback",modifiers:{"640":true}}],ref:"input",class:_vm.computedClass,style:(_vm.computedStyle),attrs:{"aria-describedby":_vm.characterCountTextId},domProps:{"value":_vm.localValue},on:{"keyup":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }return _vm.handleKeyPress.apply(null, arguments)}}},'textarea',_vm.computedAttrs,false),_vm.computedListeners)),_vm._v(" "),_c('gl-form-character-count',{attrs:{"value":_vm.value,"limit":_vm.characterCountLimit,"count-text-id":_vm.characterCountTextId},scopedSlots:_vm._u([{key:"over-limit-text",fn:function(ref){ var count = ref.count; return [_vm._t("character-count-over-limit-text",null,{"count":count})]}},{key:"remaining-count-text",fn:function(ref){ var count = ref.count; return [_vm._t("remaining-character-count-text",null,{"count":count})]}}],null,true)})],1):_c('textarea',_vm._g(_vm._b({directives:[{name:"b-visible",rawName:"v-b-visible.640",value:(_vm.visibleCallback),expression:"visibleCallback",modifiers:{"640":true}}],ref:"input",class:_vm.computedClass,style:(_vm.computedStyle),domProps:{"value":_vm.localValue},on:{"keyup":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }return _vm.handleKeyPress.apply(null, arguments)}}},'textarea',_vm.computedAttrs,false),_vm.computedListeners))}; var __vue_staticRenderFns__ = []; /* style */ const __vue_inject_styles__ = undefined; /* scoped */ const __vue_scope_id__ = undefined; /* module identifier */ const __vue_module_identifier__ = undefined; /* functional template */ const __vue_is_functional_template__ = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ const __vue_component__ = /*#__PURE__*/__vue_normalize__( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, false, undefined, undefined, undefined ); export { __vue_component__ as default };