@gitlab/ui
Version:
GitLab UI Components
543 lines (532 loc) • 16.9 kB
JavaScript
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 };