UNPKG

bootstrap-vue

Version:

With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens

583 lines (512 loc) 22.2 kB
var _watch; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import { extend } from '../../vue'; import { NAME_FORM_FILE } from '../../constants/components'; import { HAS_PROMISE_SUPPORT } from '../../constants/env'; import { EVENT_NAME_CHANGE, EVENT_OPTIONS_PASSIVE } from '../../constants/events'; import { PROP_TYPE_ARRAY, PROP_TYPE_BOOLEAN, PROP_TYPE_FUNCTION, PROP_TYPE_STRING } from '../../constants/props'; import { SLOT_NAME_DROP_PLACEHOLDER, SLOT_NAME_FILE_NAME, SLOT_NAME_PLACEHOLDER } from '../../constants/slots'; import { RX_EXTENSION, RX_STAR } from '../../constants/regex'; import { File } from '../../constants/safe-types'; import { from as arrayFrom, flatten, flattenDeep } from '../../utils/array'; import { cloneDeep } from '../../utils/clone-deep'; import { closest } from '../../utils/dom'; import { eventOn, eventOff, stopEvent } from '../../utils/events'; import { identity } from '../../utils/identity'; import { isArray, isFile, isFunction, isNull, isUndefinedOrNull } from '../../utils/inspect'; import { looseEqual } from '../../utils/loose-equal'; import { makeModelMixin } from '../../utils/model'; import { sortKeys } from '../../utils/object'; import { hasPropFunction, makeProp, makePropsConfigurable } from '../../utils/props'; import { escapeRegExp } from '../../utils/string'; import { warn } from '../../utils/warn'; import { attrsMixin } from '../../mixins/attrs'; import { formControlMixin, props as formControlProps } from '../../mixins/form-control'; import { formCustomMixin, props as formCustomProps } from '../../mixins/form-custom'; import { formStateMixin, props as formStateProps } from '../../mixins/form-state'; import { idMixin, props as idProps } from '../../mixins/id'; import { normalizeSlotMixin } from '../../mixins/normalize-slot'; import { props as formSizeProps } from '../../mixins/form-size'; // --- Constants --- var _makeModelMixin = makeModelMixin('value', { type: [PROP_TYPE_ARRAY, File], defaultValue: null, validator: function validator(value) { /* istanbul ignore next */ if (value === '') { warn(VALUE_EMPTY_DEPRECATED_MSG, NAME_FORM_FILE); return true; } return isUndefinedOrNull(value) || isValidValue(value); } }), modelMixin = _makeModelMixin.mixin, modelProps = _makeModelMixin.props, MODEL_PROP_NAME = _makeModelMixin.prop, MODEL_EVENT_NAME = _makeModelMixin.event; var VALUE_EMPTY_DEPRECATED_MSG = 'Setting "value"/"v-model" to an empty string for reset is deprecated. Set to "null" instead.'; // --- Helper methods --- var isValidValue = function isValidValue(value) { return isFile(value) || isArray(value) && value.every(function (v) { return isValidValue(v); }); }; // Helper method to "safely" get the entry from a data-transfer item /* istanbul ignore next: not supported in JSDOM */ var getDataTransferItemEntry = function getDataTransferItemEntry(item) { return isFunction(item.getAsEntry) ? item.getAsEntry() : isFunction(item.webkitGetAsEntry) ? item.webkitGetAsEntry() : null; }; // Drop handler function to get all files /* istanbul ignore next: not supported in JSDOM */ var getAllFileEntries = function getAllFileEntries(dataTransferItemList) { var traverseDirectories = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; return Promise.all(arrayFrom(dataTransferItemList).filter(function (item) { return item.kind === 'file'; }).map(function (item) { var entry = getDataTransferItemEntry(item); if (entry) { if (entry.isDirectory && traverseDirectories) { return getAllFileEntriesInDirectory(entry.createReader(), "".concat(entry.name, "/")); } else if (entry.isFile) { return new Promise(function (resolve) { entry.file(function (file) { file.$path = ''; resolve(file); }); }); } } return null; }).filter(identity)); }; // Get all the file entries (recursive) in a directory /* istanbul ignore next: not supported in JSDOM */ var getAllFileEntriesInDirectory = function getAllFileEntriesInDirectory(directoryReader) { var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; return new Promise(function (resolve) { var entryPromises = []; var readDirectoryEntries = function readDirectoryEntries() { directoryReader.readEntries(function (entries) { if (entries.length === 0) { resolve(Promise.all(entryPromises).then(function (entries) { return flatten(entries); })); } else { entryPromises.push(Promise.all(entries.map(function (entry) { if (entry) { if (entry.isDirectory) { return getAllFileEntriesInDirectory(entry.createReader(), "".concat(path).concat(entry.name, "/")); } else if (entry.isFile) { return new Promise(function (resolve) { entry.file(function (file) { file.$path = "".concat(path).concat(file.name); resolve(file); }); }); } } return null; }).filter(identity))); readDirectoryEntries(); } }); }; readDirectoryEntries(); }); }; // --- Props --- var props = makePropsConfigurable(sortKeys(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({}, idProps), modelProps), formControlProps), formCustomProps), formStateProps), formSizeProps), {}, { accept: makeProp(PROP_TYPE_STRING, ''), browseText: makeProp(PROP_TYPE_STRING, 'Browse'), // Instruct input to capture from camera capture: makeProp(PROP_TYPE_BOOLEAN, false), directory: makeProp(PROP_TYPE_BOOLEAN, false), dropPlaceholder: makeProp(PROP_TYPE_STRING, 'Drop files here'), fileNameFormatter: makeProp(PROP_TYPE_FUNCTION), multiple: makeProp(PROP_TYPE_BOOLEAN, false), noDrop: makeProp(PROP_TYPE_BOOLEAN, false), noDropPlaceholder: makeProp(PROP_TYPE_STRING, 'Not allowed'), // TODO: // Should we deprecate this and only support flat file structures? // Nested file structures are only supported when files are dropped // A Chromium "bug" prevents `webkitEntries` from being populated // on the file input's `change` event and is marked as "WontFix" // Mozilla implemented the behavior the same way as Chromium // See: https://bugs.chromium.org/p/chromium/issues/detail?id=138987 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1326031 noTraverse: makeProp(PROP_TYPE_BOOLEAN, false), placeholder: makeProp(PROP_TYPE_STRING, 'No file chosen') })), NAME_FORM_FILE); // --- Main component --- // @vue/component export var BFormFile = /*#__PURE__*/extend({ name: NAME_FORM_FILE, mixins: [attrsMixin, idMixin, modelMixin, normalizeSlotMixin, formControlMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], inheritAttrs: false, props: props, data: function data() { return { files: [], dragging: false, // IE 11 doesn't respect setting `event.dataTransfer.dropEffect`, // so we handle it ourselves as well // https://stackoverflow.com/a/46915971/2744776 dropAllowed: !this.noDrop, hasFocus: false }; }, computed: { // Convert `accept` to an array of `[{ RegExpr, isMime }, ...]` computedAccept: function computedAccept() { var accept = this.accept; accept = (accept || '').trim().split(/[,\s]+/).filter(identity); // Allow any file type/extension if (accept.length === 0) { return null; } return accept.map(function (extOrType) { var prop = 'name'; var startMatch = '^'; var endMatch = '$'; if (RX_EXTENSION.test(extOrType)) { // File extension /\.ext$/ startMatch = ''; } else { // MIME type /^mime\/.+$/ or /^mime\/type$/ prop = 'type'; if (RX_STAR.test(extOrType)) { endMatch = '.+$'; // Remove trailing `*` extOrType = extOrType.slice(0, -1); } } // Escape all RegExp special chars extOrType = escapeRegExp(extOrType); var rx = new RegExp("".concat(startMatch).concat(extOrType).concat(endMatch)); return { rx: rx, prop: prop }; }); }, computedCapture: function computedCapture() { var capture = this.capture; return capture === true || capture === '' ? true : capture || null; }, computedAttrs: function computedAttrs() { var name = this.name, disabled = this.disabled, required = this.required, form = this.form, computedCapture = this.computedCapture, accept = this.accept, multiple = this.multiple, directory = this.directory; return _objectSpread(_objectSpread({}, this.bvAttrs), {}, { type: 'file', id: this.safeId(), name: name, disabled: disabled, required: required, form: form || null, capture: computedCapture, accept: accept || null, multiple: multiple, directory: directory, webkitdirectory: directory, 'aria-required': required ? 'true' : null }); }, computedFileNameFormatter: function computedFileNameFormatter() { var fileNameFormatter = this.fileNameFormatter; return hasPropFunction(fileNameFormatter) ? fileNameFormatter : this.defaultFileNameFormatter; }, clonedFiles: function clonedFiles() { return cloneDeep(this.files); }, flattenedFiles: function flattenedFiles() { return flattenDeep(this.files); }, fileNames: function fileNames() { return this.flattenedFiles.map(function (file) { return file.name; }); }, labelContent: function labelContent() { // Draging active /* istanbul ignore next: used by drag/drop which can't be tested easily */ if (this.dragging && !this.noDrop) { return (// TODO: Add additional scope with file count, and other not-allowed reasons this.normalizeSlot(SLOT_NAME_DROP_PLACEHOLDER, { allowed: this.dropAllowed }) || (this.dropAllowed ? this.dropPlaceholder : this.$createElement('span', { staticClass: 'text-danger' }, this.noDropPlaceholder)) ); } // No file chosen if (this.files.length === 0) { return this.normalizeSlot(SLOT_NAME_PLACEHOLDER) || this.placeholder; } var flattenedFiles = this.flattenedFiles, clonedFiles = this.clonedFiles, fileNames = this.fileNames, computedFileNameFormatter = this.computedFileNameFormatter; // There is a slot for formatting the files/names if (this.hasNormalizedSlot(SLOT_NAME_FILE_NAME)) { return this.normalizeSlot(SLOT_NAME_FILE_NAME, { files: flattenedFiles, filesTraversed: clonedFiles, names: fileNames }); } return computedFileNameFormatter(flattenedFiles, clonedFiles, fileNames); } }, watch: (_watch = {}, _defineProperty(_watch, MODEL_PROP_NAME, function (newValue) { if (!newValue || isArray(newValue) && newValue.length === 0) { this.reset(); } }), _defineProperty(_watch, "files", function files(newValue, oldValue) { if (!looseEqual(newValue, oldValue)) { var multiple = this.multiple, noTraverse = this.noTraverse; var files = !multiple || noTraverse ? flattenDeep(newValue) : newValue; this.$emit(MODEL_EVENT_NAME, multiple ? files : files[0] || null); } }), _watch), created: function created() { // Create private non-reactive props this.$_form = null; }, mounted: function mounted() { // Listen for form reset events, to reset the file input var $form = closest('form', this.$el); if ($form) { eventOn($form, 'reset', this.reset, EVENT_OPTIONS_PASSIVE); this.$_form = $form; } }, beforeDestroy: function beforeDestroy() { var $form = this.$_form; if ($form) { eventOff($form, 'reset', this.reset, EVENT_OPTIONS_PASSIVE); } }, methods: { isFileValid: function isFileValid(file) { if (!file) { return false; } var accept = this.computedAccept; return accept ? accept.some(function (a) { return a.rx.test(file[a.prop]); }) : true; }, isFilesArrayValid: function isFilesArrayValid(files) { var _this = this; return isArray(files) ? files.every(function (file) { return _this.isFileValid(file); }) : this.isFileValid(files); }, defaultFileNameFormatter: function defaultFileNameFormatter(flattenedFiles, clonedFiles, fileNames) { return fileNames.join(', '); }, setFiles: function setFiles(files) { // Reset the dragging flags this.dropAllowed = !this.noDrop; this.dragging = false; // Set the selected files this.files = this.multiple ? this.directory ? files : flattenDeep(files) : flattenDeep(files).slice(0, 1); }, /* istanbul ignore next: used by Drag/Drop */ setInputFiles: function setInputFiles(files) { // Try an set the file input files array so that `required` // constraint works for dropped files (will fail in IE11 though) // To be used only when dropping files try { // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655 var dataTransfer = new ClipboardEvent('').clipboardData || new DataTransfer(); // Add flattened files to temp `dataTransfer` object to get a true `FileList` array flattenDeep(cloneDeep(files)).forEach(function (file) { // Make sure to remove the custom `$path` attribute delete file.$path; dataTransfer.items.add(file); }); this.$refs.input.files = dataTransfer.files; } catch (_unused) {} }, reset: function reset() { // IE 11 doesn't support setting `$input.value` to `''` or `null` // So we use this little extra hack to reset the value, just in case // This also appears to work on modern browsers as well // Wrapped in try in case IE 11 or mobile Safari crap out try { var $input = this.$refs.input; $input.value = ''; $input.type = ''; $input.type = 'file'; } catch (_unused2) {} this.files = []; }, handleFiles: function handleFiles(files) { var isDrop = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (isDrop) { // When dropped, make sure to filter files with the internal `accept` logic var filteredFiles = files.filter(this.isFilesArrayValid); // Only update files when we have any after filtering if (filteredFiles.length > 0) { this.setFiles(filteredFiles); // Try an set the file input's files array so that `required` // constraint works for dropped files (will fail in IE 11 though) this.setInputFiles(filteredFiles); } } else { // We always update the files from the `change` event this.setFiles(files); } }, focusHandler: function focusHandler(event) { // Bootstrap v4 doesn't have focus styling for custom file input // Firefox has a `[type=file]:focus ~ sibling` selector issue, // so we add a `focus` class to get around these bugs if (this.plain || event.type === 'focusout') { this.hasFocus = false; } else { // Add focus styling for custom file input this.hasFocus = true; } }, onChange: function onChange(event) { var _this2 = this; var type = event.type, target = event.target, _event$dataTransfer = event.dataTransfer, dataTransfer = _event$dataTransfer === void 0 ? {} : _event$dataTransfer; var isDrop = type === 'drop'; // Always emit original event this.$emit(EVENT_NAME_CHANGE, event); var items = arrayFrom(dataTransfer.items || []); if (HAS_PROMISE_SUPPORT && items.length > 0 && !isNull(getDataTransferItemEntry(items[0]))) { // Drop handling for modern browsers // Supports nested directory structures in `directory` mode /* istanbul ignore next: not supported in JSDOM */ getAllFileEntries(items, this.directory).then(function (files) { return _this2.handleFiles(files, isDrop); }); } else { // Standard file input handling (native file input change event), // or fallback drop mode (IE 11 / Opera) which don't support `directory` mode var files = arrayFrom(target.files || dataTransfer.files || []).map(function (file) { // Add custom `$path` property to each file (to be consistent with drop mode) file.$path = file.webkitRelativePath || ''; return file; }); this.handleFiles(files, isDrop); } }, onDragenter: function onDragenter(event) { stopEvent(event); this.dragging = true; var _event$dataTransfer2 = event.dataTransfer, dataTransfer = _event$dataTransfer2 === void 0 ? {} : _event$dataTransfer2; // Early exit when the input or dropping is disabled if (this.noDrop || this.disabled || !this.dropAllowed) { // Show deny feedback /* istanbul ignore next: not supported in JSDOM */ dataTransfer.dropEffect = 'none'; this.dropAllowed = false; return; } /* istanbul ignore next: not supported in JSDOM */ dataTransfer.dropEffect = 'copy'; }, // Note this event fires repeatedly while the mouse is over the dropzone at // intervals in the milliseconds, so avoid doing much processing in here onDragover: function onDragover(event) { stopEvent(event); this.dragging = true; var _event$dataTransfer3 = event.dataTransfer, dataTransfer = _event$dataTransfer3 === void 0 ? {} : _event$dataTransfer3; // Early exit when the input or dropping is disabled if (this.noDrop || this.disabled || !this.dropAllowed) { // Show deny feedback /* istanbul ignore next: not supported in JSDOM */ dataTransfer.dropEffect = 'none'; this.dropAllowed = false; return; } /* istanbul ignore next: not supported in JSDOM */ dataTransfer.dropEffect = 'copy'; }, onDragleave: function onDragleave(event) { var _this3 = this; stopEvent(event); this.$nextTick(function () { _this3.dragging = false; // Reset `dropAllowed` to default _this3.dropAllowed = !_this3.noDrop; }); }, // Triggered by a file drop onto drop target onDrop: function onDrop(event) { var _this4 = this; stopEvent(event); this.dragging = false; // Early exit when the input or dropping is disabled if (this.noDrop || this.disabled || !this.dropAllowed) { this.$nextTick(function () { // Reset `dropAllowed` to default _this4.dropAllowed = !_this4.noDrop; }); return; } this.onChange(event); } }, render: function render(h) { var custom = this.custom, plain = this.plain, size = this.size, dragging = this.dragging, stateClass = this.stateClass, bvAttrs = this.bvAttrs; // Form Input var $input = h('input', { class: [{ 'form-control-file': plain, 'custom-file-input': custom, focus: custom && this.hasFocus }, stateClass], // With IE 11, the input gets in the "way" of the drop events, // so we move it out of the way by putting it behind the label // Bootstrap v4 has it in front style: custom ? { zIndex: -5 } : {}, attrs: this.computedAttrs, on: { change: this.onChange, focusin: this.focusHandler, focusout: this.focusHandler, reset: this.reset }, ref: 'input' }); if (plain) { return $input; } // Overlay label var $label = h('label', { staticClass: 'custom-file-label', class: { dragging: dragging }, attrs: { for: this.safeId(), // This goes away in Bootstrap v5 'data-browse': this.browseText || null } }, [h('span', { staticClass: 'd-block form-file-text', // `pointer-events: none` is used to make sure // the drag events fire only on the label style: { pointerEvents: 'none' } }, [this.labelContent])]); // Return rendered custom file input return h('div', { staticClass: 'custom-file b-form-file', class: [_defineProperty({}, "b-custom-control-".concat(size), size), stateClass, bvAttrs.class], style: bvAttrs.style, attrs: { id: this.safeId('_BV_file_outer_') }, on: { dragenter: this.onDragenter, dragover: this.onDragover, dragleave: this.onDragleave, drop: this.onDrop } }, [$input, $label]); } });