UNPKG

@ishitatsuyuki/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

1,454 lines (1,441 loc) 536 kB
/*! Oruga v0.5.5-direct-export | MIT License | github.com/oruga-ui/oruga */ import { defineComponent, openBlock, createBlock, Fragment, createCommentVNode, resolveDynamicComponent, resolveComponent, mergeProps, createVNode, toDisplayString, withKeys, withModifiers, Transition, withCtx, withDirectives, renderSlot, renderList, vShow, createTextVNode, vModelCheckbox, h, resolveDirective, Comment, Text, vModelSelect, createSlots, render as render$B, toHandlers, vModelRadio, createApp, capitalize } from 'vue'; /** * +/- function to native math sign */ function signPoly(value) { if (value < 0) return -1; return value > 0 ? 1 : 0; } const sign = Math.sign || signPoly; /** * Checks if the flag is set * @param val * @param flag * @returns {boolean} */ function hasFlag(val, flag) { return (val & flag) === flag; } /** * Native modulo bug with negative numbers * @param n * @param mod * @returns {number} */ function mod(n, mod) { return ((n % mod) + mod) % mod; } /** * Asserts a value is beetween min and max * @param val * @param min * @param max * @returns {number} */ function bound(val, min, max) { return Math.max(min, Math.min(max, val)); } /** * Get value of an object property/path even if it's nested */ function getValueByPath(obj, path, defaultValue = undefined) { const value = path.split('.').reduce((o, i) => typeof o !== 'undefined' ? o[i] : undefined, obj); return typeof value !== 'undefined' ? value : defaultValue; } /** * Extension of indexOf method by equality function if specified */ function indexOf(array, obj, fn) { if (!array) return -1; if (!fn || typeof fn !== 'function') return array.indexOf(obj); for (let i = 0; i < array.length; i++) { if (fn(array[i], obj)) { return i; } } return -1; } /** * Merge function to replace Object.assign with deep merging possibility */ const isObject = (item) => typeof item === 'object' && !Array.isArray(item); const mergeFn = (target, source, deep = false) => { if (deep || !Object.assign) { const isDeep = (prop) => isObject(source[prop]) && target !== null && Object.prototype.hasOwnProperty.call(target, prop) && isObject(target[prop]); let replaced; if (source === null || typeof source === 'undefined') { replaced = false; } else { replaced = Object.getOwnPropertyNames(source) .map((prop) => ({ [prop]: isDeep(prop) ? mergeFn(target[prop], source[prop], deep) : source[prop] })) .reduce((a, b) => ({ ...a, ...b }), {}); } return { ...target, ...replaced }; } else { return Object.assign(target, source); } }; const merge = mergeFn; /** * Mobile detection * https://www.abeautifulsite.net/detecting-mobile-devices-with-javascript */ const isMobile = { Android: function () { return (typeof window !== 'undefined' && window.navigator.userAgent.match(/Android/i)); }, BlackBerry: function () { return (typeof window !== 'undefined' && window.navigator.userAgent.match(/BlackBerry/i)); }, iOS: function () { return (typeof window !== 'undefined' && window.navigator.userAgent.match(/iPhone|iPad|iPod/i)); }, Opera: function () { return (typeof window !== 'undefined' && window.navigator.userAgent.match(/Opera Mini/i)); }, Windows: function () { return (typeof window !== 'undefined' && window.navigator.userAgent.match(/IEMobile/i)); }, any: function () { return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows()); } }; function removeElement(el) { if (typeof el.remove !== 'undefined') { el.remove(); } else if (typeof el.parentNode !== 'undefined' && el.parentNode !== null) { el.parentNode.removeChild(el); } } function createAbsoluteElement(el) { const root = document.createElement('div'); root.style.position = 'absolute'; root.style.left = '0px'; root.style.top = '0px'; const wrapper = document.createElement('div'); root.appendChild(wrapper); wrapper.appendChild(el); document.body.appendChild(root); return root; } /** * Escape regex characters * http://stackoverflow.com/a/6969486 */ function escapeRegExpChars(value) { if (!value) return value; return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } function toCssDimension(width) { return width === undefined ? null : (isNaN(width) ? width : width + 'px'); } function blankIfUndefined(value) { return typeof value !== 'undefined' && value !== null ? value : ''; } function defaultIfUndefined(value, defaultValue) { return typeof value !== 'undefined' && value !== null ? value : defaultValue; } function getMonthNames(locale = undefined, format = 'long') { const dates = []; for (let i = 0; i < 12; i++) { dates.push(new Date(2000, i, 15)); } const dtf = new Intl.DateTimeFormat(locale, { month: format, }); return dates.map((d) => dtf.format(d)); } function getWeekdayNames(locale = undefined, firstDayOfWeek = 0, format = 'narrow') { const dates = []; for (let i = 1, j = 0; j < 7; i++) { const d = new Date(2000, 0, i); const day = d.getDay(); if (day === firstDayOfWeek || j > 0) { dates.push(d); j++; } } const dtf = new Intl.DateTimeFormat(locale, { weekday: format, }); return dates.map((d) => dtf.format(d)); } /** * Accept a regex with group names and return an object * ex. matchWithGroups(/((?!=<year>)\d+)\/((?!=<month>)\d+)\/((?!=<day>)\d+)/, '2000/12/25') * will return { year: 2000, month: 12, day: 25 } * @param {String} includes injections of (?!={groupname}) for each group * @param {String} the string to run regex * @return {Object} an object with a property for each group having the group's match as the value */ function matchWithGroups(pattern, str) { const matches = str.match(pattern); return pattern // get the pattern as a string .toString() // suss out the groups .match(/<(.+?)>/g) // remove the braces .map((group) => { const groupMatches = group.match(/<(.+)>/); if (!groupMatches || groupMatches.length <= 0) { return null; } return group.match(/<(.+)>/)[1]; }) // create an object with a property for each group having the group's match as the value .reduce((acc, curr, index) => { if (matches && matches.length > index) { acc[curr] = matches[index + 1]; } else { acc[curr] = null; } return acc; }, {}); } function debounce(func, wait, immediate) { let timeout; return function () { const context = this; const args = arguments; const later = function () { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } const isDefined = (d) => d !== undefined; /** * Remove accents/diacritics in a string in JavaScript * https://stackoverflow.com/a/37511463 */ function removeDiacriticsFromString(value) { if (!value) return value; return value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } let config = { iconPack: 'mdi', useHtml5Validation: true, statusIcon: true, transformClasses: undefined }; const setOptions = (options) => { config = options; }; const getOptions = () => { return config; }; let VueInstance; const setVueInstance = (Vue) => { VueInstance = Vue; }; const Programmatic = { getOptions, setOptions(options) { setOptions(merge(getOptions(), options, true)); } }; const Plugin = { install(Vue, options = {}) { setVueInstance(Vue); // Options setOptions(merge(getOptions(), options, true)); } }; const _defaultSuffixProcessor = (input, suffix) => { return blankIfUndefined(input) .split(' ') .filter((cls) => cls.length > 0) .map((cls) => cls + suffix) .join(' '); }; const _getContext = (vm) => { const computedNames = vm.$options.computed ? Object.keys(vm.$options.computed) : []; const computed = computedNames.filter(e => !endsWith(e, 'Classes')).reduce((o, key) => { o[key] = vm[key]; return o; }, {}); return { props: vm.$props, data: vm.$data, computed }; }; var BaseComponentMixin = defineComponent({ isOruga: true, props: { override: Boolean }, methods: { computedClass(field, defaultValue, suffix = '') { const config = this.$props.override === true ? {} : getOptions(); const override = this.$props.override || getValueByPath(config, `${this.$options.configField}.override`, false); const overrideClass = getValueByPath(config, `${this.$options.configField}.${field}.override`, override); const globalTransformClasses = getValueByPath(config, `transformClasses`, undefined); const localTransformClasses = getValueByPath(config, `${this.$options.configField}.transformClasses`, undefined); let globalClass = getValueByPath(config, `${this.$options.configField}.${field}.class`, '') || getValueByPath(config, `${this.$options.configField}.${field}`, ''); let currentClass = getValueByPath(this.$props, field); if (Array.isArray(currentClass)) { currentClass = currentClass.join(' '); } if (defaultValue.search("{*}") !== -1) { defaultValue = defaultValue.replace(/\{\*\}/g, suffix); } else { defaultValue = defaultValue + suffix; } let context = null; if (typeof currentClass === "function") { context = _getContext(this); currentClass = currentClass(suffix, context); } else { currentClass = _defaultSuffixProcessor(currentClass, suffix); } if (typeof globalClass === "function") { globalClass = globalClass(suffix, context || _getContext(this)); } else { globalClass = _defaultSuffixProcessor(globalClass, suffix); } let appliedClasses = (`${(override && !overrideClass) || (!override && !overrideClass) ? defaultValue : ''} ` + `${blankIfUndefined(globalClass)} ` + `${blankIfUndefined(currentClass)}`).trim().replace(/\s\s+/g, ' '); if (localTransformClasses) { appliedClasses = localTransformClasses(appliedClasses); } if (globalTransformClasses) { appliedClasses = globalTransformClasses(appliedClasses); } return appliedClasses; } } }); const mdiIcons = { sizes: { 'default': 'mdi-24px', 'small': null, 'medium': 'mdi-36px', 'large': 'mdi-48px' }, iconPrefix: 'mdi-' }; const faIcons = () => { const iconComponent = getValueByPath(getOptions(), 'iconComponent'); const faIconPrefix = iconComponent ? '' : 'fa-'; return { sizes: { 'default': null, 'small': null, 'medium': faIconPrefix + 'lg', 'large': faIconPrefix + '2x' }, iconPrefix: faIconPrefix, internalIcons: { 'check': 'check', 'information': 'info-circle', 'alert': 'exclamation-triangle', 'alert-circle': 'exclamation-circle', 'arrow-up': 'arrow-up', 'chevron-right': 'angle-right', 'chevron-left': 'angle-left', 'chevron-down': 'angle-down', 'chevron-up': 'angle-up', 'eye': 'eye', 'eye-off': 'eye-slash', 'caret-down': 'caret-down', 'caret-up': 'caret-up', 'close-circle': 'times-circle', 'close': 'times', 'loading': 'circle-notch' } }; }; const getIcons = () => { let icons = { mdi: mdiIcons, fa: faIcons(), fas: faIcons(), far: faIcons(), fad: faIcons(), fab: faIcons(), fal: faIcons() }; const customIconPacks = getValueByPath(getOptions(), 'customIconPacks'); if (customIconPacks) { icons = merge(icons, customIconPacks, true); } return icons; }; /** * Icons take an important role of any application * @displayName Icon * @style _icon.scss */ var script = defineComponent({ name: 'OIcon', mixins: [BaseComponentMixin], configField: 'icon', props: { /** * Color of the icon, optional * @values primary, info, success, warning, danger, and any other custom color */ variant: [String, Object], /** * Icon component name */ component: String, /** * Icon pack to use * @values mdi, fa, fas and any other custom icon pack */ pack: String, /** * Icon name */ icon: String, /** * Icon size, optional * @values small, medium, large */ size: String, /** * Overrides icon font size, optional * @values Depends on library: null (smallest), fa-lg, fa-2x, fa-3x, fa-4x, fa-5x, mdi-18px, mdi-24px, mdi-36px, mdi-48px */ customSize: String, /** * Add class to icon font, optional. See here for MDI, here for FontAwesome 4 and here for FontAwesome 5 custom classes */ customClass: String, /** * When true makes icon clickable */ clickable: Boolean, /** Enable spin effect on icon */ spin: Boolean, /** Rotation 0-360 */ rotation: [Number, String], /** @ignore */ both: Boolean, rootClass: [String, Function, Array], clickableClass: [String, Function, Array], spinClass: [String, Function, Array], sizeClass: [String, Function, Array], variantClass: [String, Function, Array] }, computed: { rootClasses() { return [ this.computedClass('rootClass', 'o-icon'), { [this.computedClass('clickableClass', 'o-icon--clickable')]: this.clickable }, { [this.computedClass('spinClass', 'o-icon--spin')]: this.spin }, { [this.computedClass('sizeClass', 'o-icon--', this.size)]: this.size }, { [this.computedClass('variantClass', 'o-icon--', this.newVariant)]: this.newVariant } ]; }, rootStyle() { const style = {}; if (this.rotation) { style['transform'] = `rotate(${this.rotation}deg)`; } return style; }, iconConfig() { return getIcons()[this.newPack]; }, iconPrefix() { if (this.iconConfig && this.iconConfig.iconPrefix) { return this.iconConfig.iconPrefix; } return ''; }, /** * Internal icon name based on the pack. * If pack is 'fa', gets the equivalent FA icon name of the MDI, * internal icons are always MDI. */ newIcon() { return `${this.iconPrefix}${this.getEquivalentIconOf(this.icon)}`; }, newPack() { return this.pack || getValueByPath(getOptions(), 'iconPack', 'mdi'); }, newVariant() { if (!this.variant) return; let newVariant = ''; if (typeof this.variant === 'string') { newVariant = this.variant; } else { newVariant = Object.keys(this.variant).filter(key => this.variant[key])[0]; } return newVariant; }, newCustomSize() { return this.customSize || this.customSizeByPack; }, customSizeByPack() { if (this.iconConfig && this.iconConfig.sizes) { if (this.size && this.iconConfig.sizes[this.size] !== undefined) { return this.iconConfig.sizes[this.size]; } else if (this.iconConfig.sizes.default) { return this.iconConfig.sizes.default; } } return null; }, useIconComponent() { if (this.component) return this.component; const component = getValueByPath(getOptions(), 'iconComponent'); if (component) return component; return null; } }, methods: { /** * Equivalent icon name of the MDI. */ getEquivalentIconOf(value) { // Only transform the class if the both prop is set to true if (!this.both) { return value; } if (this.iconConfig && this.iconConfig.internalIcons && this.iconConfig.internalIcons[value]) { return this.iconConfig.internalIcons[value]; } return value; } } }); function render(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createBlock("span", { class: _ctx.rootClasses, style: _ctx.rootStyle }, [!_ctx.useIconComponent ? (openBlock(), createBlock("i", { key: 0, class: [_ctx.newPack, _ctx.newIcon, _ctx.newCustomSize, _ctx.customClass] }, null, 2 /* CLASS */ )) : (openBlock(), createBlock(Fragment, { key: 1 }, [createCommentVNode(" custom icon component "), (openBlock(), createBlock(resolveDynamicComponent(_ctx.useIconComponent), { icon: [_ctx.newPack, _ctx.newIcon], size: _ctx.newCustomSize, class: [_ctx.customClass] }, null, 8 /* PROPS */ , ["icon", "size", "class"]))], 64 /* STABLE_FRAGMENT */ ))], 6 /* CLASS, STYLE */ ); } script.render = render; script.__file = "src/components/icon/Icon.vue"; var FormElementMixin = defineComponent({ inject: { $field: { from: "$field", default: false } }, emits: ['blur', 'focus'], props: { /** * Makes input full width when inside a grouped or addon field */ expanded: Boolean, /** * Makes the element rounded */ rounded: Boolean, /** * Icon name to be added */ icon: String, /** * Icon pack to use * @values mdi, fa, fas and any other custom icon pack */ iconPack: String, /** Native options to use in HTML5 validation */ autocomplete: String, /** Same as native maxlength, plus character counter */ maxlength: [Number, String], /** Enable html 5 native validation */ useHtml5Validation: { type: Boolean, default: () => { return getValueByPath(getOptions(), "useHtml5Validation", true); }, }, /** Show status icon using field and variant prop */ statusIcon: { type: Boolean, default: () => { return getValueByPath(getOptions(), "statusIcon", true); }, }, /** * The message which is shown when a validation error occurs */ validationMessage: String, }, data() { return { isValid: true, isFocused: false, newIconPack: this.iconPack, }; }, computed: { parentField() { return this.$field; }, /** * Get the type prop from parent if it's a Field. */ statusVariant() { if (!this.parentField) return; if (!this.parentField.newVariant) return; if (typeof this.parentField.newVariant === "string") { return this.parentField.newVariant; } else { for (const key in this.parentField.newVariant) { if (this.parentField.newVariant[key]) { return key; } } } }, /** * Get the message prop from parent if it's a Field. */ statusMessage() { if (!this.parentField) return; return this.parentField.newMessage || this.parentField.hasMessageSlot; }, /** * Icon name based on the variant. */ statusVariantIcon() { const statusVariantIcon = getValueByPath(getOptions(), "statusVariantIcon", { 'success': 'check', 'danger': 'alert-circle', 'info': 'information', 'warning': 'alert' }); return statusVariantIcon[this.statusVariant] || ''; } }, methods: { /** * Focus method that work dynamically depending on the component. */ focus() { const el = this.getElement(); if (!el) return; this.$nextTick(() => { if (el) el.focus(); }); }, onBlur(event) { this.isFocused = false; if (this.parentField) { this.parentField.isFocused = false; } this.$emit("blur", event); this.checkHtml5Validity(); }, onFocus(event) { this.isFocused = true; if (this.parentField) { this.parentField.isFocused = true; } this.$emit("focus", event); }, getElement() { let el = this.$refs[this.$elementRef]; while (el && el.$elementRef) { el = el.$refs[el.$elementRef]; } return el; }, setInvalid() { const variant = "danger"; const message = this.validationMessage || this.getElement().validationMessage; this.setValidity(variant, message); }, setValidity(variant, message) { this.$nextTick(() => { if (this.parentField) { // Set type only if not defined if (!this.parentField.variant) { this.parentField.newVariant = variant; } // Set message only if not defined if (!this.parentField.message) { this.parentField.newMessage = message; } } }); }, /** * Check HTML5 validation, set isValid property. * If validation fail, send 'danger' type, * and error message to parent if it's a Field. */ checkHtml5Validity() { if (!this.useHtml5Validation) return; const el = this.getElement(); if (!el) return; if (!el.checkValidity()) { this.setInvalid(); this.isValid = false; } else { this.setValidity(null, null); this.isValid = true; } return this.isValid; }, syncFilled(value) { if (this.parentField) { this.parentField.isFilled = !!value; } } } }); /** * Get user Input. Use with Field to access all functionalities * @displayName Input * @style _input.scss */ var script$1 = defineComponent({ name: 'OInput', components: { [script.name]: script }, mixins: [BaseComponentMixin, FormElementMixin], configField: 'input', inheritAttrs: false, emits: ['update:modelValue', 'icon-click', 'icon-right-click'], props: { /** @model */ modelValue: [Number, String], /** Native options to use in HTML5 validation */ autocomplete: String, /** * Input type, like native * @values Any native input type, and textarea */ type: { type: String, default: 'text' }, /** * Vertical size of input, optional * @values small, medium, large */ size: String, /** * Color of the control, optional * @values primary, info, success, warning, danger, and any other custom color */ variant: String, /** * Adds the reveal password functionality */ passwordReveal: Boolean, /** * Makes the icon clickable */ iconClickable: Boolean, /** * Show character counter when maxlength prop is passed */ hasCounter: { type: Boolean, default: () => { return getValueByPath(getOptions(), 'input.counter', false); } }, /** * Automatically adjust height in textarea */ autosize: { type: Boolean, default: false }, /** * Icon name to be added on the right side */ iconRight: String, /** * Make the icon right clickable */ iconRightClickable: Boolean, /** Variant of right icon */ iconRightVariant: String, /** Add a button/icon to clear the inputed text */ clearable: { type: Boolean, default: () => { return getValueByPath(getOptions(), 'input.clearable', false); } }, rootClass: [String, Function, Array], expandedClass: [String, Function, Array], iconLeftSpaceClass: [String, Function, Array], iconRightSpaceClass: [String, Function, Array], inputClass: [String, Function, Array], roundedClass: [String, Function, Array], iconLeftClass: [String, Function, Array], iconRightClass: [String, Function, Array], counterClass: [String, Function, Array], sizeClass: [String, Function, Array], variantClass: [String, Function, Array] }, data() { return { newValue: this.modelValue, newType: this.type, // from mixin (ts workaround) newAutocomplete: this.autocomplete || getValueByPath(getOptions(), 'input.autocompletete', 'off'), isPasswordVisible: false, height: 'auto' }; }, computed: { rootClasses() { return [ this.computedClass('rootClass', 'o-ctrl-input'), { [this.computedClass('expandedClass', 'o-ctrl-input--expanded')]: this.expanded } ]; }, inputClasses() { return [ this.computedClass('inputClass', 'o-input'), { [this.computedClass('roundedClass', 'o-input--rounded')]: this.rounded }, { [this.computedClass('sizeClass', 'o-input--', this.size)]: this.size }, { [this.computedClass('variantClass', 'o-input--', (this.statusVariant || this.variant))]: (this.statusVariant || this.variant) }, { [this.computedClass('textareaClass', 'o-input__textarea')]: this.type === 'textarea' }, { [this.computedClass('iconLeftSpaceClass', 'o-input-iconspace-left')]: this.icon }, { [this.computedClass('iconRightSpaceClass', 'o-input-iconspace-right')]: this.hasIconRight } ]; }, iconLeftClasses() { return [ this.computedClass('iconLeftClass', 'o-input__icon-left') ]; }, iconRightClasses() { return [ this.computedClass('iconRightClass', 'o-input__icon-right') ]; }, counterClasses() { return [ this.computedClass('counterClass', 'o-input__counter') ]; }, computedValue: { get() { return this.newValue; }, set(value) { this.newValue = value; this.$emit('update:modelValue', this.newValue); this.syncFilled(this.newValue); !this.isValid && this.checkHtml5Validity(); } }, hasIconRight() { return this.passwordReveal || (this.statusIcon && this.statusVariantIcon) || (this.clearable && this.newValue) || this.iconRight; }, rightIcon() { if (this.passwordReveal) { return this.passwordVisibleIcon; } else if (this.clearable && this.newValue) { return 'close-circle'; } else if (this.iconRight) { return this.iconRight; } return this.statusVariantIcon; }, rightIconVariant() { if (this.passwordReveal || this.iconRight) { return this.iconRightVariant || this.variant || null; } return this.statusVariant; }, /** * Check if have any message prop from parent if it's a Field. */ hasMessage() { return !!this.statusMessage; }, /** * Current password-reveal icon name. */ passwordVisibleIcon() { return !this.isPasswordVisible ? 'eye' : 'eye-off'; }, /** * Get value length */ valueLength() { if (typeof this.computedValue === 'string') { return this.computedValue.length; } else if (typeof this.computedValue === 'number') { return this.computedValue.toString().length; } return 0; }, /** * Computed inline styles for autoresize */ computedStyles() { if (!this.autosize) return {}; return { resize: 'none', height: this.height, overflow: 'hidden' }; }, $elementRef() { return this.type === 'textarea' ? 'textarea' : 'input'; } }, watch: { /** * When v-model is changed: * 1. Set internal value. */ modelValue: { immediate: true, handler(value) { this.newValue = value; this.syncFilled(this.newValue); if (this.autosize) { this.resize(); } } } }, methods: { /** * Toggle the visibility of a password-reveal input * by changing the type and focus the input right away. */ togglePasswordVisibility() { this.isPasswordVisible = !this.isPasswordVisible; this.newType = this.isPasswordVisible ? 'text' : 'password'; this.$nextTick(() => { this.focus(); }); }, iconClick(emit, event) { this.$emit(emit, event); this.$nextTick(() => { this.focus(); }); }, rightIconClick(event) { if (this.passwordReveal) { this.togglePasswordVisibility(); } else if (this.clearable) { this.computedValue = ''; } else if (this.iconRightClickable) { this.iconClick('icon-right-click', event); } }, resize() { this.height = 'auto'; this.$nextTick(() => { let scrollHeight = this.$refs.textarea.scrollHeight; this.height = scrollHeight + 'px'; }); } } }); function render$1(_ctx, _cache, $props, $setup, $data, $options) { const _component_o_icon = resolveComponent("o-icon"); return openBlock(), createBlock("div", { class: _ctx.rootClasses }, [_ctx.type !== 'textarea' ? (openBlock(), createBlock("input", mergeProps({ key: 0 }, _ctx.$attrs, { ref: "input", class: _ctx.inputClasses, type: _ctx.newType, autocomplete: _ctx.newAutocomplete, maxlength: _ctx.maxlength, value: _ctx.computedValue, onInput: _cache[1] || (_cache[1] = $event => _ctx.computedValue = $event.target.value), onBlur: _cache[2] || (_cache[2] = (...args) => _ctx.onBlur(...args)), onFocus: _cache[3] || (_cache[3] = (...args) => _ctx.onFocus(...args)) }), null, 16 /* FULL_PROPS */ , ["type", "autocomplete", "maxlength", "value"])) : (openBlock(), createBlock("textarea", mergeProps({ key: 1 }, _ctx.$attrs, { ref: "textarea", class: _ctx.inputClasses, maxlength: _ctx.maxlength, value: _ctx.computedValue, onInput: _cache[4] || (_cache[4] = $event => _ctx.computedValue = $event.target.value), onBlur: _cache[5] || (_cache[5] = (...args) => _ctx.onBlur(...args)), onFocus: _cache[6] || (_cache[6] = (...args) => _ctx.onFocus(...args)), style: _ctx.computedStyles }), null, 16 /* FULL_PROPS */ , ["maxlength", "value"])), _ctx.icon ? createVNode(_component_o_icon, { key: 2, class: _ctx.iconLeftClasses, clickable: _ctx.iconClickable, icon: _ctx.icon, pack: _ctx.iconPack, size: _ctx.size, onClick: _cache[7] || (_cache[7] = $event => _ctx.iconClick('icon-click', $event)) }, null, 8 /* PROPS */ , ["class", "clickable", "icon", "pack", "size"]) : createCommentVNode("v-if", true), _ctx.hasIconRight ? createVNode(_component_o_icon, { key: 3, class: _ctx.iconRightClasses, clickable: _ctx.passwordReveal || _ctx.clearable || _ctx.iconRightClickable, icon: _ctx.rightIcon, pack: _ctx.iconPack, size: _ctx.size, variant: _ctx.rightIconVariant, both: "", onClick: _ctx.rightIconClick }, null, 8 /* PROPS */ , ["class", "clickable", "icon", "pack", "size", "variant", "onClick"]) : createCommentVNode("v-if", true), _ctx.maxlength && _ctx.hasCounter && _ctx.isFocused && _ctx.type !== 'number' ? (openBlock(), createBlock("small", { key: 4, class: _ctx.counterClasses }, toDisplayString(_ctx.valueLength) + " / " + toDisplayString(_ctx.maxlength), 3 /* TEXT, CLASS */ )) : createCommentVNode("v-if", true)], 2 /* CLASS */ ); } script$1.render = render$1; script$1.__file = "src/components/input/Input.vue"; /** * Extended input that provide suggestions while the user types * @displayName Autocomplete * @style _autocomplete.scss */ var script$2 = defineComponent({ name: 'OAutocomplete', configField: 'autocomplete', components: { [script$1.name]: script$1 }, mixins: [BaseComponentMixin, FormElementMixin], inheritAttrs: false, emits: ['update:modelValue', 'select', 'infinite-scroll', 'typing', 'focus', 'blur', 'icon-click', 'icon-right-click'], props: { /** @model */ modelValue: [Number, String], /** Options / suggestions */ data: { type: Array, default: () => [] }, /** Native options to use in HTML5 validation */ autocomplete: String, /** * Vertical size of input, optional * @values small, medium, large */ size: String, /** Property of the object (if data is array of objects) to use as display text, and to keep track of selected option */ field: { type: String, default: 'value' }, /** The first option will always be pre-selected (easier to just hit enter or tab) */ keepFirst: Boolean, /** Clear input text on select */ clearOnSelect: Boolean, /** Open dropdown list on focus */ openOnFocus: Boolean, /** Function to format an option to a string for display in the input as alternative to field prop) */ customFormatter: Function, /** Makes the component check if list reached scroll end and emit infinite-scroll event. */ checkInfiniteScroll: Boolean, /** Keep open dropdown list after select */ keepOpen: Boolean, /** Add a button/icon to clear the inputed text */ clearable: Boolean, /** Max height of dropdown content */ maxHeight: [String, Number], /** * Position of dropdown * @values auto, top, bottom */ menuPosition: { type: String, default: 'auto' }, /** Transition name to apply on dropdown list */ animation: { type: String, default: () => { return getValueByPath(getOptions(), 'autocomplete.animation', 'fade'); } }, /** Property of the object (if <code>data</code> is array of objects) to use as display text of group */ groupField: String, /** Property of the object (if <code>data</code> is array of objects) to use as key to get items array of each group, optional */ groupOptions: String, /** Number of milliseconds to delay before to emit typing event */ debounceTyping: Number, /** Icon name to be added on the right side */ iconRight: String, /** Clickable icon right if exists */ iconRightClickable: Boolean, /** Append autocomplete content to body */ appendToBody: Boolean, /** Array of keys (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) which will add a tag when typing (default tab and enter) */ confirmKeys: { type: Array, default: () => ['Tab', 'Enter'] }, /** Input type */ type: { type: String, default: 'text' }, /** * Menu tag name */ menuTag: { type: String, default: () => { return getValueByPath(getOptions(), 'autocomplete.menuTag', 'div'); } }, /** * Menu item tag name */ itemTag: { type: String, default: () => { return getValueByPath(getOptions(), 'autocomplete.itemTag', 'div'); } }, /** Trigger the select event for the first pre-selected option when clicking outside and <code>keep-first</code> is enabled */ selectOnClickOutside: Boolean, /** Allows the header in the autocomplete to be selectable */ selectableHeader: Boolean, /** Allows the footer in the autocomplete to be selectable */ selectableFooter: Boolean, rootClass: [String, Function, Array], menuClass: [String, Function, Array], expandedClass: [String, Function, Array], menuPositionClass: [String, Function, Array], itemClass: [String, Function, Array], itemHoverClass: [String, Function, Array], itemGroupTitleClass: [String, Function, Array], itemEmptyClass: [String, Function, Array], itemHeaderClass: [String, Function, Array], itemFooterClass: [String, Function, Array], inputClasses: { type: Object, default: () => { return getValueByPath(getOptions(), 'autocomplete.inputClasses', {}); } } }, data() { return { selected: null, hovered: null, headerHovered: null, footerHovered: null, isActive: false, newValue: this.modelValue, ariaAutocomplete: this.keepFirst ? 'both' : 'list', newAutocomplete: this.autocomplete || 'off', isListInViewportVertically: true, hasFocus: false, itemRefs: [], width: undefined, bodyEl: undefined, }; }, computed: { rootClasses() { return [ this.computedClass('rootClass', 'o-acp'), { [this.computedClass('expandedClass', 'o-acp--expanded')]: this.expanded } ]; }, menuClasses() { return [ this.computedClass('menuClass', 'o-acp__menu'), { [this.computedClass('menuPositionClass', 'o-acp__menu--', this.newDropdownPosition)]: !this.appendToBody }, ]; }, itemClasses() { return [ this.computedClass('itemClass', 'o-acp__item') ]; }, itemEmptyClasses() { return [ ...this.itemClasses, this.computedClass('itemEmptyClass', 'o-acp__item--empty') ]; }, itemGroupClasses() { return [ ...this.itemClasses, this.computedClass('itemGroupTitleClass', 'o-acp__item-group-title') ]; }, itemHeaderClasses() { return [ ...this.itemClasses, this.computedClass('itemHeaderClass', 'o-acp__item-header'), { [this.computedClass('itemHoverClass', 'o-acp__item--hover')]: this.headerHovered } ]; }, itemFooterClasses() { return [ ...this.itemClasses, this.computedClass('itemFooterClass', 'o-acp__item-footer'), { [this.computedClass('itemHoverClass', 'o-acp__item--hover')]: this.footerHovered } ]; }, inputBind() { return { ...this.$attrs, ...this.inputClasses }; }, computedData() { if (this.groupField) { if (this.groupOptions) { const newData = []; this.data.forEach((option) => { const group = getValueByPath(option, this.groupField); const items = getValueByPath(option, this.groupOptions); newData.push({ group, items }); }); return newData; } else { const tmp = {}; this.data.forEach((option) => { const group = getValueByPath(option, this.groupField); if (!tmp[group]) tmp[group] = []; tmp[group].push(option); }); const newData = []; Object.keys(this.data).forEach((group) => { newData.push({ group, items: this.data[group] }); }); return newData; } } return [{ items: this.data }]; }, isEmpty() { if (!this.computedData) return true; return !this.computedData.some((element) => element.items && element.items.length); }, /** * White-listed items to not close when clicked. * Add input, dropdown and all children. */ whiteList() { const whiteList = []; whiteList.push(this.$refs.input.$el.querySelector('input')); whiteList.push(this.$refs.dropdown); // Add all children from dropdown if (this.$refs.dropdown !== undefined) { const children = this.$refs.dropdown.querySelectorAll('*'); for (const child of children) { whiteList.push(child); } } return whiteList; }, newDropdownPosition() { if (this.menuPosition === 'top' || (this.menuPosition === 'auto' && !this.isListInViewportVertically)) { return 'top'; } return 'bottom'; }, newIconRight() { if (this.clearable && this.newValue) { return 'close-circle'; } return this.iconRight; }, newIconRightClickable() { if (this.clearable) { return true; } return this.iconRightClickable; }, menuStyle() { return { maxHeight: toCssDimension(this.maxHeight) }; }, $elementRef() { return 'input'; } }, watch: { /** * When v-model is changed: * 1. Update internal value. * 2. If it's invalid, validate again. */ modelValue(value) { this.newValue = value; }, /** * When dropdown is toggled, check the visibility to know when * to open upwards. */ isActive(active) { if (this.menuPosition === 'auto') { if (active) { this.calcDropdownInViewportVertical(); } else { // Timeout to wait for the animation to finish before recalculating setTimeout(() => { this.calcDropdownInViewportVertical(); }, 100); } } }, /** * When updating input's value * 1. Emit changes * 2. If value isn't the same as selected, set null * 3. Close dropdown if value is clear or else open it */ newValue(value) { this.$emit('update:modelValue', value); // Check if selected is invalid const currentValue = this.getValue(this.selected); if (currentValue && currentValue !== value) { this.setSelected(null, false); } // Close dropdown if input is clear or else open it if (this.hasFocus && (!this.openOnFocus || value)) { this.isActive = !!value; } }, /** * Select first option if "keep-first */ data() { // Keep first option always pre-selected if (this.keepFirst) { this.$nextTick(() => { if (this.isActive) { this.selectFirstOption(this.computedData); } else { this.setHovered(null); } }); } else { if (this.hovered) { // reset hovered if list doesn't contain it const hoveredValue = this.getValue(this.hovered); const data = this.computedData.map((d) => d.items).reduce((a, b) => ([...a, ...b]), []); if (!data.some(d => this.getValue(d) === hoveredValue)) { this.setHovered(null); } } } }, debounceTyping: { handler(value) { this.debouncedEmitTyping = debounce(this.emitTyping, value); }, immediate: true } }, methods: { itemOptionClasses(option) { return [ ...this.itemClasses, { [this.computedClass('itemHoverClass', 'o-acp__item--hover')]: option === this.hovered } ]; }, /** * Set which option is currently hovered. */ setHovered(option) { if (option === undefined) return; this.hovered = option; }, /** * Set which option is currently selected, update v-model, * update input value and close dropdown. */ setSelected(option, cl