Skip to content

Commit

Permalink
Merge pull request #2748 from sascha-karnatz/add-multi-tag-component
Browse files Browse the repository at this point in the history
Transform the tag selector into a web component
  • Loading branch information
tvdeyen authored Feb 27, 2024
2 parents bd8ca2a + 56c74d1 commit cdfee90
Show file tree
Hide file tree
Showing 19 changed files with 181 additions and 92 deletions.
25 changes: 25 additions & 0 deletions app/components/alchemy/admin/tags_autocomplete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Alchemy
module Admin
class TagsAutocomplete < ViewComponent::Base
delegate :alchemy, to: :helpers

def initialize(additional_class: nil)
@additional_class = additional_class
end

def call
content_tag("alchemy-tags-autocomplete", content, attributes)
end

private

def attributes
{
placeholder: Alchemy.t(:search_tag),
url: alchemy.autocomplete_admin_tags_path,
class: @additional_class
}
end
end
end
end
1 change: 1 addition & 0 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import "alchemy_admin/components/overlay"
import "alchemy_admin/components/page_select"
import "alchemy_admin/components/select"
import "alchemy_admin/components/spinner"
import "alchemy_admin/components/tags_autocomplete"
import "alchemy_admin/components/tinymce"

import { setDefaultAnimation } from "shoelace"
Expand Down
2 changes: 0 additions & 2 deletions app/javascript/alchemy_admin/components/element_editor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import TagsAutocomplete from "alchemy_admin/tags_autocomplete"
import ImageLoader from "alchemy_admin/image_loader"
import fileEditors from "alchemy_admin/file_editors"
import pictureEditors from "alchemy_admin/picture_editors"
Expand Down Expand Up @@ -44,7 +43,6 @@ export class ElementEditor extends HTMLElement {
`#${this.id} .ingredient-editor.file, #${this.id} .ingredient-editor.audio, #${this.id} .ingredient-editor.video`
)
pictureEditors(`#${this.id} .ingredient-editor.picture`)
TagsAutocomplete(this)
}

handleEvent(event) {
Expand Down
57 changes: 57 additions & 0 deletions app/javascript/alchemy_admin/components/tags_autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class TagsAutocomplete extends HTMLElement {
connectedCallback() {
this.classList.add("autocomplete_tag_list")
$(this.input).select2(this.select2Config)
}

get input() {
return this.getElementsByTagName("input")[0]
}

get select2Config() {
return {
tags: true,
tokenSeparators: [","],
openOnEnter: false,
minimumInputLength: 1,
createSearchChoice: this.#createSearchChoice,
ajax: {
url: this.getAttribute("url"),
dataType: "json",
data: (term) => {
return { term }
},
results: (data) => {
return { results: data }
}
},
initSelection: this.#initSelection
}
}

#createSearchChoice(term, data) {
if (
$(data).filter(function () {
return this.text.localeCompare(term) === 0
}).length === 0
) {
return {
id: term,
text: term
}
}
}

#initSelection(element, callback) {
const data = []
$(element.val().split(",")).each(function () {
data.push({
id: this.trim(),
text: this
})
})
callback(data)
}
}

customElements.define("alchemy-tags-autocomplete", TagsAutocomplete)
3 changes: 0 additions & 3 deletions app/javascript/alchemy_admin/gui.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import TagsAutocomplete from "alchemy_admin/tags_autocomplete"

function init(scope) {
if (!scope) {
Alchemy.watchForDialogs()
}
Alchemy.Hotkeys(scope)
Alchemy.ListFilter(scope)
TagsAutocomplete(scope)
}

export default {
Expand Down
46 changes: 0 additions & 46 deletions app/javascript/alchemy_admin/tags_autocomplete.js

This file was deleted.

7 changes: 3 additions & 4 deletions app/views/alchemy/admin/attachments/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
url: {action: :update, q: search_filter_params[:q], page: params[:page]}) do |f| -%>
<%= f.input :name, input_html: {autofocus: true} %>
<%= f.input :file_name, input_html: {autofocus: true}, hint: Alchemy.t(:attachment_filename_notice) %>
<div class="input string autocomplete_tag_list">
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
</div>
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
<%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
<% end %>
<%= f.submit Alchemy.t(:save) %>
<% end %>
6 changes: 3 additions & 3 deletions app/views/alchemy/admin/elements/_element.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@
<% end %>
<% if element.taggable? %>
<div class="autocomplete_tag_list">
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
</div>
<%= f.text_field :tag_list, value: f.object.tag_list.join(",") %>
<% end %>
<% end %>
<% end %>
Expand Down
7 changes: 3 additions & 4 deletions app/views/alchemy/admin/layoutpages/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<%= alchemy_form_for [:admin, @page], class: 'edit_page' do |f| %>
<%= f.input :name, autofocus: true %>
<div class="input string">
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
</div>
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
<%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
<% end %>
<%= f.submit Alchemy.t(:save) %>
<% end %>
7 changes: 3 additions & 4 deletions app/views/alchemy/admin/pages/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,9 @@
as: 'text',
hint: Alchemy.t('pages.update.comma_seperated') %>
<div class="input string autocomplete_tag_list">
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
</div>
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
<%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
<% end %>
<%= f.submit Alchemy.t(:save) %>
<% end %>

This file was deleted.

6 changes: 3 additions & 3 deletions app/views/alchemy/admin/pictures/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<%= alchemy_form_for [:admin, @picture] do |f| %>
<%= f.input :name %>
<%= render "picture_description_field", f: f %>
<div class="input tag_list">
<%= render Alchemy::Admin::TagsAutocomplete.new(additional_class: "input") do %>
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
<%= f.text_field :tag_list, value: f.object.tag_list.join(",") %>
<small class="hint"><%= Alchemy.t('Please seperate the tags with commata') %></small>
</div>
<% end %>
<%= hidden_field_tag :q, search_filter_params[:q] %>
<%= hidden_field_tag :size, @size %>
<%= hidden_field_tag :tagged_with, search_filter_params[:tagged_with] %>
Expand Down
6 changes: 3 additions & 3 deletions app/views/alchemy/admin/pictures/edit_multiple.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
<%= text_field_tag :pictures_name %>
</div>

<div class="input tag_list">
<%= render Alchemy::Admin::TagsAutocomplete.new(additional_class: "input") do %>
<%= label_tag :pictures_tag_list, Alchemy.t(:tags), class: 'control-label' %>
<%= text_field_tag :pictures_tag_list, @tags, data: {autocomplete: alchemy.autocomplete_admin_tags_path} %>
<%= text_field_tag :pictures_tag_list, @tags %>
<small class="hint"><%= Alchemy.t('Please seperate the tags with commata') %></small>
</div>
<% end %>

<div class="submit">
<%= button_tag Alchemy.t(:save), name: nil, class: 'button' %>
Expand Down
7 changes: 3 additions & 4 deletions app/views/alchemy/admin/resources/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
<% end %>
<% end %>
<% if f.object.respond_to?(:tag_list) %>
<div class="input string autocomplete_tag_list">
<%= f.label :tag_list %>
<%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %>
</div>
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
<%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
<% end %>
<% end %>
<%= f.submit Alchemy.t(:save) %>
<% end %>
2 changes: 2 additions & 0 deletions config/locales/alchemy.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,9 @@ en:
delete_node: "Delete this menu node"
delete_page: "Delete this page"
delete_tag: "Delete tag"
search_page: "Search page"
search_node: "Search menu node"
search_tag: "Search tag"
document: "File"
download_csv: "Download CSV"
download_file: "Download file '%{filename}'"
Expand Down
36 changes: 36 additions & 0 deletions spec/components/alchemy/admin/tags_autocomplete_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "rails_helper"

RSpec.describe Alchemy::Admin::TagsAutocomplete, type: :component do
before do
render
end

context "without parameters" do
subject(:render) do
render_inline(described_class.new) { "Page Select Content" }
end

it "should render the component and render given block content" do
expect(page).to have_selector("alchemy-tags-autocomplete")
expect(page).to have_text("Page Select Content")
end

it "should have the default placeholder" do
expect(page).to have_selector("alchemy-tags-autocomplete[placeholder='Search tag']")
end

it "should have the default tags autocomplete - url" do
expect(page).to have_selector("alchemy-tags-autocomplete[url='/admin/tags/autocomplete']")
end
end

context "with additional class" do
subject(:render) do
render_inline(described_class.new(additional_class: "foooo"))
end

it "should have these class" do
expect(page).to have_selector("alchemy-tags-autocomplete.foooo")
end
end
end
2 changes: 1 addition & 1 deletion spec/features/admin/resources_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
context "with event that acts_as_taggable" do
it "shows an autocomplete tag list in the form" do
visit "/admin/events/new"
expect(page).to have_selector('input#event_tag_list[type="text"][data-autocomplete="/admin/tags/autocomplete"]')
expect(page).to have_selector('alchemy-tags-autocomplete input#event_tag_list[type="text"]')
end

context "with tagged events in the index view" do
Expand Down
14 changes: 0 additions & 14 deletions spec/javascript/alchemy_admin/components/element_editor.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import TagsAutocomplete from "alchemy_admin/tags_autocomplete"
import ImageLoader from "alchemy_admin/image_loader"
import fileEditors from "alchemy_admin/file_editors"
import pictureEditors from "alchemy_admin/picture_editors"
import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
import { ElementEditor } from "alchemy_admin/components/element_editor"
import { renderComponent } from "./component.helper"

jest.mock("alchemy_admin/tags_autocomplete", () => {
return {
__esModule: true,
default: jest.fn()
}
})

jest.mock("alchemy_admin/image_loader", () => {
return {
__esModule: true,
Expand Down Expand Up @@ -141,7 +133,6 @@ describe("alchemy-element-editor", () => {

describe("connectedCallback", () => {
beforeEach(() => {
TagsAutocomplete.mockClear()
ImageLoader.init.mockClear()
fileEditors.mockClear()
pictureEditors.mockClear()
Expand Down Expand Up @@ -171,11 +162,6 @@ describe("alchemy-element-editor", () => {
getComponent(html)
expect(pictureEditors).toHaveBeenCalled()
})

it("initializes tags autocomplete", () => {
getComponent(html)
expect(TagsAutocomplete).toHaveBeenCalled()
})
})

describe("on click", () => {
Expand Down
38 changes: 38 additions & 0 deletions spec/javascript/alchemy_admin/components/tags_autocomplete.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { renderComponent } from "./component.helper"

// import jquery and append it to the window object
import jQuery from "jquery"
globalThis.$ = jQuery
globalThis.jQuery = jQuery

import "alchemy_admin/components/tags_autocomplete"
import("assets/jquery_plugins/select2")

describe("alchemy-tags-autocomplete", () => {
/**
*
* @type {HTMLElement | undefined}
*/
let component = undefined

beforeEach(() => {
const html = `
<alchemy-tags-autocomplete>
<input type="text">
</alchemy-tags-autocomplete>
`
component = renderComponent("alchemy-tags-autocomplete", html)
})

it("should render the input field", () => {
expect(component.getElementsByTagName("input")[0]).toBeInstanceOf(
HTMLElement
)
})

it("should initialize Select2", () => {
expect(
component.getElementsByClassName("select2-container").length
).toEqual(1)
})
})

0 comments on commit cdfee90

Please sign in to comment.