UNPKG

tagelect

Version:

Tag input field with autocomplete and validations.

509 lines (362 loc) 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; require("core-js/stable"); require("regenerator-runtime/runtime"); var _axios = require("axios"); var _lodash = _interopRequireDefault(require("lodash.merge")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function Tagelect(element, options) { var _this5 = this; this.element = element; this.defaultOptions = { // max amount of tags that can be added // (it is possible it initially render more by setting input value) maxTags: undefined, maxTagsError: "can't contain more than %TAGS% tags", validationRegex: undefined, // the regex that is going to be used while validating a tag validationRegexError: 'permitted characters used', // the message shown if regexp validation fails noDuplicates: false, // are duplicates allowed noDuplicatesError: 'duplicates are not allowed', // the message shown if validations fail because of duplicates placeholder: 'Enter a tag...', // The placeholder for the tag entry removeButton: true, // Show remove button tagDelimiter: ',', // The delimiter used when setting form input value suggestionsCount: 5, // Fetch and display 5 suggestions suggestionHeaders: function suggestionHeaders() { return {}; }, // Function returning headers for suggestion GET request suggestionsSource: null, // CSS classes classNames: { wrapper: 'tagelect', container: 'tagelect-content', tagInput: 'tagelect-input', tag: 'tagelect-tag', removeButton: 'tagelect-tag-remove-btn', tagText: 'tagelect-tag-text', dropdown: 'tagelect-dropdown', dropdownItem: 'tagelect-dropdown-item', dropdownItemSelected: 'tagelect-dropdown-item--selected', error: 'tagelect-error' } }; this.options = (0, _lodash["default"])(this.defaultOptions, options); this.tags = []; this.suggestions = []; // If input has a value, try to initialize tags from it // by reading the value, splitting on delimiter and trimming the tag text if (this.element.value) { // Set the initial tags from hidden input value this.tags = this.element.value.split(this.options.tagDelimiter).map(function (e) { return e.trim(); }); } // Hide element this.element.style.display = 'none'; // Serialize the current tags to a string (using the delimiter) and set it as input value function setInputValue() { this.element.value = this.tags.join(this.options.tagDelimiter); } this.setInputValue = setInputValue.bind(this); function tagelectContainerElement() { return this.element.parentElement.querySelector('[data-tagelect-container]'); } this.tagelectContainerElement = tagelectContainerElement.bind(this); function tabCompleteAllowed(suggestion) { var container = this.tagelectContainerElement(); var tagInput = container.querySelector('[data-tagelect-tag-input]'); if (!suggestion || tagInput.innerText.length === 0) { return false; } return suggestion.startsWith(tagInput.innerText); } this.tabCompleteAllowed = tabCompleteAllowed.bind(this); function setFirstSuggestion(suggestion) { var container = this.tagelectContainerElement(); var tagInput = container.querySelector('[data-tagelect-tag-input]'); if (suggestion) { tagInput.dataset.suggestion = suggestion.replace(tagInput.innerText, ''); } else { tagInput.removeAttribute('data-suggestion'); } } this.setFirstSuggestion = setFirstSuggestion.bind(this); function removeValidationErrors() { this.containerElement.parentElement.querySelectorAll('[data-tagelect-error]').forEach(function (e) { e.remove(); }); } this.removeValidationErrors = removeValidationErrors.bind(this); function optionClasses(name) { return this.options.classNames[name].split(' '); } this.optionClasses = optionClasses.bind(this); function createTagelectElement(htmlTag, name) { var _tagelectElement$clas; var tagelectElement = document.createElement(htmlTag); var elementClasses = this.optionClasses(name); (_tagelectElement$clas = tagelectElement.classList).add.apply(_tagelectElement$clas, _toConsumableArray(elementClasses)); tagelectElement.dataset["tagelect".concat(name[0].toUpperCase()).concat(name.slice(1))] = true; return tagelectElement; } this.createTagelectElement = createTagelectElement.bind(this); function buildErrorSpan(errorMessage) { var errorElement = this.createTagelectElement('span', 'error'); errorElement.innerText = errorMessage; return errorElement; } this.buildErrorSpan = buildErrorSpan.bind(this); function appendError(errorMessage) { var errorElement = this.buildErrorSpan(errorMessage); this.containerElement.insertAdjacentElement('afterend', errorElement); } this.appendError = appendError.bind(this); function validateStuff(nextTag) { // Remove previous errors this.removeValidationErrors(); var maxTags = parseInt(this.options.maxTags, 10); // Error if max tags limit reached if (this.options.maxTags && this.tags.length === maxTags) { // The maxTagsError may contain templating variable %TAGS% this.appendError(this.options.maxTagsError.replace('%TAGS%', maxTags)); return false; } // Error if tag does not match validation regex if (this.options.validationRegex && !nextTag.match(this.options.validationRegex)) { this.appendError(this.options.validationRegexError); return false; } if (this.options.noDuplicates && this.tags.includes(nextTag)) { this.appendError(this.options.noDuplicatesError); return false; } return true; } this.validateStuff = validateStuff.bind(this); function renderRemoveButton() { var _this = this; var removeBtnElement = this.createTagelectElement('div', 'removeButton'); removeBtnElement.innerText = '×'; // &times; icon removeBtnElement.addEventListener('click', function (e) { e.preventDefault(); var tag = e.target.closest('[data-tagelect-tag]'); var removedTagText = tag.querySelector('[data-tagelect-tag-text]').innerText; // Removing tag from state _this.tags = _this.tags.filter(function (tg) { return tg !== removedTagText; }); // Clear the tag input _this.setInputValue(); // Remove the tag from DOM tag.remove(); }); return removeBtnElement; } this.renderRemoveButton = renderRemoveButton.bind(this); // Tag rendering function renderTag(text) { var container = this.tagelectContainerElement(); // Build a tag var tagElement = this.createTagelectElement('div', 'tag'); var tagTextElement = this.createTagelectElement('div', 'tagText'); tagTextElement.innerHTML = text; tagElement.insertAdjacentElement('beforeend', tagTextElement); if (this.options.removeButton) { var removeButtonElement = this.renderRemoveButton(); tagElement.insertAdjacentElement('beforeend', removeButtonElement); } var tagInput = container.querySelector('[data-tagelect-tag-input]'); // Prepend tag to the tags tagInput.insertAdjacentElement('beforebegin', tagElement); } this.renderTag = renderTag.bind(this); function renderTags() { var _this2 = this; var container = this.tagelectContainerElement(); var tagElements = container.querySelectorAll('[data-tagelect-tag]'); // Remove previously rendered tags tagElements.forEach(function (tagElem) { return tagElem.remove(); }); if (this.tags.length === 0) { return; } this.tags.forEach(function (tagText) { _this2.renderTag(tagText); }); } this.renderTags = renderTags.bind(this); function focusOnTagInput() { var tagInput = this.containerElement.querySelector('[data-tagelect-tag-input]'); var clickEvent = new Event('click'); tagInput.dispatchEvent(clickEvent); } this.focusOnTagInput = focusOnTagInput.bind(this); function clearTagInput() { var tagInput = this.containerElement.querySelector('[data-tagelect-tag-input]'); tagInput.innerHTML = ''; } this.clearTagInput = clearTagInput.bind(this); function toggleDropdown(show) { var _this3 = this; var prevDropdown = document.querySelector('[data-tagelect-dropdown]'); // Remove previous dropdown if (prevDropdown) { prevDropdown.remove(); } if (!show) { return; } // Else re-build a new dropdown var dropdownElement = this.createTagelectElement('ul', 'dropdown'); this.suggestions.forEach(function (suggestion, idx) { var itemElement = _this3.createTagelectElement('li', 'dropdownItem'); if (idx === 0) { var _itemElement$classLis; (_itemElement$classLis = itemElement.classList).add.apply(_itemElement$classLis, _toConsumableArray(_this3.options.classNames.dropdownItemSelected.split(' '))); } itemElement.innerText = suggestion; itemElement.addEventListener('click', function (e) { if (!_this3.validateStuff(e.target.innerText)) { return; } _this3.tags.push(e.target.innerText); _this3.renderTags(); _this3.setInputValue(); _this3.clearTagInput(); _this3.focusOnTagInput(); _this3.toggleDropdown(false); }); dropdownElement.insertAdjacentElement('beforeend', itemElement); }); this.containerElement.insertAdjacentElement('afterend', dropdownElement); } this.toggleDropdown = toggleDropdown.bind(this); function fetchSuggestions(searchPhrase) { var _this4 = this; var count = this.options.suggestionsCount; if (!this.options.suggestionsSource) { return; } var url = "".concat(this.options.suggestionsSource, "?name=").concat(searchPhrase, "&count=").concat(count); var headers = this.options.suggestionHeaders(); (0, _axios.get)(url, { headers: headers }).then(function (response) { _this4.suggestions = response.data === null ? [] : response.data; var firstSuggestion = _this4.suggestions[0] || null; // If typed "a" and the source returned "Alabama" the data-suggestion should // not be set, because the resulting Tab-completion would look like: // "aAlabama" if (!_this4.tabCompleteAllowed(firstSuggestion)) { firstSuggestion = null; } _this4.setFirstSuggestion(firstSuggestion); // If no suggestions found - don't show dropdown _this4.toggleDropdown(_this4.suggestions.length > 0); })["catch"](function () { _this4.setFirstSuggestion(null); _this4.toggleDropdown(false); }); } this.fetchSuggestions = fetchSuggestions.bind(this); // Render initial tagelect elements this.wrapperElement = this.createTagelectElement('div', 'wrapper'); this.wrapperElement.setAttribute('tabindex', '-1'); // Track if mouse is in container (used for during blur event handling) this.wrapperElement.addEventListener('mouseover', function () { _this5.mouseOverContainer = true; }); this.wrapperElement.addEventListener('mouseout', function () { _this5.mouseOverContainer = false; }); this.containerElement = this.createTagelectElement('div', 'container'); // Render the input field for entering tags var tagInputElement = this.createTagelectElement('span', 'tagInput'); tagInputElement.setAttribute('contenteditable', true); tagInputElement.dataset.placeholder = this.options.placeholder; tagInputElement.style.minWidth = "".concat(this.options.placeholder.length * 6, "px"); ['click', 'focus'].forEach(function (eventType) { tagInputElement.addEventListener(eventType, function (evt) { if (evt.target.innerText.length > 0) { _this5.fetchSuggestions(evt.target.innerText); } }); }); tagInputElement.addEventListener('blur', function () { _this5.suggestions = []; var dropdown = document.querySelector('[data-tagelect-dropdown]'); // Do nothing unless dropdown is open if (!dropdown) { return; } // If mouse is outside of the whole container if (!_this5.mouseOverContainer) { _this5.setFirstSuggestion(null); _this5.toggleDropdown(false); } }); // Process the keydown events for tag input tagInputElement.addEventListener('keydown', function (e) { // Add the suggestion as tag -> if Tab is pressed while there is a valid suggestion if (e.key === 'Tab' && _this5.tabCompleteAllowed(_this5.suggestions[0])) { e.preventDefault(); var tagText = _this5.suggestions[0]; if (!_this5.validateStuff(tagText)) { return; } _this5.tags = _this5.tags.concat(tagText); _this5.renderTags(); _this5.setInputValue(); _this5.clearTagInput(); _this5.suggestions = []; _this5.toggleDropdown(false); _this5.focusOnTagInput(); } // Add tag -> if Enter pressed in non-empty input if ((e.key === 'Enter' || e.key === ',') && e.target.innerText.length > 0) { e.preventDefault(); if (!_this5.validateStuff(e.target.innerText)) { return; } _this5.tags = _this5.tags.concat(e.target.innerText); _this5.renderTags(); _this5.setInputValue(); _this5.clearTagInput(); _this5.suggestions = []; _this5.toggleDropdown(false); _this5.focusOnTagInput(); return; } // Close dropdown -> if no tags and tag input is cleared if (e.key === 'Backspace' && e.target.innerText.length === 1 && _this5.tags.length === 0) { _this5.removeValidationErrors(); _this5.toggleDropdown(false); return; } // Remove last tag -> if Backspace pressed in empty tag input if (e.key === 'Backspace' && e.target.innerText.length === 0 && _this5.tags.length > 0) { _this5.removeValidationErrors(); _this5.tags.pop(); _this5.setInputValue(); _this5.renderTags(); return; } // Do nothing -> if Enter pressed in empty tag input if (e.key === 'Enter' && e.target.innerText.length === 0) { e.preventDefault(); } var alphanumeric = e.key.match(/^\w$/); // Update tag input and fetch suggestions -> if alphanum. entered or char deleted // // NOTE: The event hasn't changed the innerText yet, so we have to check for length > 1 if (alphanumeric || e.key === 'Backspace' && e.target.innerText.length > 1) { var tagInput = _this5.containerElement.querySelector('[data-tagelect-tag-input]'); var newText = e.key === 'Backspace' ? tagInput.innerText.slice(0, -1) : tagInput.innerText + e.key; if (!_this5.validateStuff(newText)) { return; } _this5.fetchSuggestions(newText); } if (e.key === 'Backspace' && e.target.innerText.length === 1) { _this5.removeValidationErrors(); _this5.toggleDropdown(false); } }); // Add elements to DOM this.containerElement.insertAdjacentElement('beforeend', tagInputElement); this.wrapperElement.insertAdjacentElement('beforeend', this.containerElement); this.element.insertAdjacentElement('beforebegin', this.wrapperElement); // Render the initial tags this.renderTags(); } var _default = Tagelect; exports["default"] = _default;