diff --git a/app/components/ui/tag_input/component.rb b/app/components/ui/tag_input/component.rb new file mode 100644 index 00000000..2000ad35 --- /dev/null +++ b/app/components/ui/tag_input/component.rb @@ -0,0 +1,25 @@ +class Ui::TagInput::Component < ApplicationComponent + def initialize(placeholder: nil, value: nil, **options) + @placeholder = placeholder + @value = value + @options = options + end + + def call + tag.input(type: "text", value: @value, placeholder: @placeholder, class: input_classes, **attrs) + end + + private + + def attrs + data_attributes = ({ controller: "tagify" }).deep_merge(@options.fetch(:data, {})) + @options.merge(data: data_attributes) + end + + def input_classes + class_names( + "tagify-input w-1/2 border border-gray-300 rounded px-3 py-2", + @options.delete(:class) + ) + end +end diff --git a/app/components/ui/tag_input/tagify_controller.js b/app/components/ui/tag_input/tagify_controller.js new file mode 100644 index 00000000..e54542a4 --- /dev/null +++ b/app/components/ui/tag_input/tagify_controller.js @@ -0,0 +1,77 @@ +import { Controller } from '@hotwired/stimulus' +import Tagify from '@yaireo/tagify' +import { stimulus } from '~/init' +import '@yaireo/tagify/dist/tagify.css' + +export default class TagifyController extends Controller { + get tagifyInstance () { + const { mode, whitelist, maxtags } = this.element.dataset + + return new Tagify(this.element, { + tagTextProp: 'name', + editTags: false, + mode: mode || null, + dropdown: { + enabled: 0, + maxItems: 100, + placeAbove: false, + fuzzySearch: false + }, + whitelist: whitelist?.split(',') || '[]', + maxTags: maxtags || 100 + }) + } + + connect () { + this.tagify = this.tagifyInstance + this.#setupInputListener() + this.tagify.on('dropdown:select', this.#onSelectSuggestion.bind(this)) + this.tagify.setContentEditable(!this.tagify.hasMaxTags()) + this.tagify.on('change', () => { + this.tagify.setContentEditable(!this.tagify.hasMaxTags()) + this.dispatch('tagifyChange', { detail: { tagify: this.tagify } }) + }) + } + + disconnect () { + this.tagify.off('input') + } + + reload () { + this.disconnect() + this.connect() + } + + #setupInputListener () { + let controller = new AbortController() + + this.tagify.on('input', (event) => { + const path = this.element.dataset.tagsPath + + if (path) { + const { value } = event.detail + this.tagify.settings.whitelist.length = 0 + + controller.abort() + controller = new AbortController() + this.tagify.loading(true) + + fetch(`${path}?query=${value}`, { signal: controller.signal }) + .then((response) => response.json()) + .then((whitelist) => { + this.tagify.whitelist = whitelist + this.tagify.loading(false).dropdown.show(value) + }) + } + }) + } + + #onSelectSuggestion (event) { + if (event.detail.elm.classList.contains('tagify__dropdown__item__addNew')) { + this.tagify.addTags(this.tagify.state.inputText) + this.tagify.DOM.input.innerHTML = '' + } + } +} + +stimulus.register('tagify', TagifyController) diff --git a/app/views/home/ui.html.erb b/app/views/home/ui.html.erb index 2e9d46e9..90d0481f 100644 --- a/app/views/home/ui.html.erb +++ b/app/views/home/ui.html.erb @@ -399,6 +399,10 @@ <% end %> <% end %> + <%= accordion.with_item(title: "Tags", id: "tags") do %> + <%= render Ui::TagInput::Component.new(placeholder: "Choose your stack", value: "", data: { whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte,C,C#,C++" }) %> + <% end %> + <%= accordion.with_item(title: "Tooltip", id: "tooltip", class: "flex gap-4 items-center") do %> <%= render Ui::Tooltip::Component.new(tooltip_content: "Tooltip content", data: { placement: "bottom" }) do %> <%= render Ui::Btn::Component.new(variant: :primary).with_content "Tooltip Button" %> diff --git a/package.json b/package.json index 842225c5..4141cb0d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.2", "@rails/request.js": "^0.0.9", + "@yaireo/tagify": "^4.34.0", "flowbite": "^3.1.2", "mjml": "^4.15.3", "stimulus-rails-autosave": "^5.1.0", diff --git a/test/components/previews/ui/tag_input/component_preview.rb b/test/components/previews/ui/tag_input/component_preview.rb new file mode 100644 index 00000000..a787d3f7 --- /dev/null +++ b/test/components/previews/ui/tag_input/component_preview.rb @@ -0,0 +1,11 @@ +class Ui::TagInput::ComponentPreview < ViewComponent::Preview + # @param placeholder + # @param default_value + # @param whitelist + # @param maxTags number + # @param closeOnSelect toggle + + def default(placeholder: "Choose your stack", default_value: "Ruby", whitelist: "Rails,Hotwire,Turbo,Stimulus,Tailwind,CSS,HTML,React,JS,Python,Svelte", maxTags: 100, closeOnSelect: false) + render(Ui::TagInput::Component.new(placeholder: placeholder, value: default_value, data: { whitelist: whitelist, maxTags: maxTags, closeOnSelect: closeOnSelect })) + end +end diff --git a/yarn.lock b/yarn.lock index 3cecc66e..e533ec2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -499,6 +499,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@yaireo/tagify@^4.34.0": + version "4.34.0" + resolved "https://registry.yarnpkg.com/@yaireo/tagify/-/tagify-4.34.0.tgz#6f058612c9b8d8470a08d24f4072ba9f8cbe8d3e" + integrity sha512-+IckJUhvjii37n/YFX3YUTJvcp8rUU0CnjAqa87/psSKjiF5xwoK7Mo0rsadzdrPxycrcn6Uwzl8zPO6/8CXOQ== + abbrev@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf"