mycowriter 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39c5a2d07b1a5b470a372a3164de8055d040d75a2dc0688b89ddc00312b182ac
4
- data.tar.gz: 28b25751676dfd32ebd30605a7431a7a6572bbaa31fea296a2414372d8ce15fb
3
+ metadata.gz: e1a1c921c961ded2607e45978d58795ee8f92bfef1ac34faa8256ff3b5d47a2d
4
+ data.tar.gz: be73f75a71d970fef1a51f2a9a2d8b1882afb7103cd204f0828e3c3acbc85d3c
5
5
  SHA512:
6
- metadata.gz: a5ba7679b9d27b760637e7becfa3bf91f1820d4fe29fb587b2e32f446bf734481778a5e837b0ed4e48797b2bd709dade21fe45d3f0b950862f0837e71d3b5af7
7
- data.tar.gz: e2dfde5dd18f9d103e91a13a12436a09163653d1a600834802cbbb5ef7ac93f48571a5c6ac29b29a30f7904cd69ed9eb0112f7ab2b99bc6b2df598166733f387
6
+ metadata.gz: 6b984d4be064516a2a60285fb81c477a611b18d3a71b97772bf3a159a1629b2dc41030b26eb980eb80cf6059ea92134c2349ca4ab8172a4273743344e67186a3
7
+ data.tar.gz: 044acf422c1eb53015b38cfbb1680df25b156635331310be9f78ea404126bf8e0802c077f06cb0d80bd60c34455d78d40d43224051ef1b311b6b6cb753931a30
@@ -110,6 +110,9 @@ module Mycowriter
110
110
  end
111
111
  end
112
112
 
113
+ # Only return legitimate names (exclude illegitimate/invalid names)
114
+ scope = scope.where(name_status: 'Legitimate') if ::MbList.column_names.include?('name_status')
115
+
113
116
  scope
114
117
  .select(:id, :taxon_name)
115
118
  .order(
@@ -1,270 +1,277 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
- // Mycowriter Autocomplete Controller
4
- // Provides token-based autocomplete for genus/species names
5
- //
6
- // Usage: Attach data-controller="mycowriter--autocomplete" and appropriate data attributes.
7
- // Requirements: An autocomplete endpoint that returns [{id, name}] in JSON.
3
+ // Inline Autocomplete Controller for textarea fields
4
+ // Provides genus/species name suggestions while typing in prose
5
+ // Inserts selected text at cursor position
6
+ // Primary use case: Helping users spell genus/species names correctly in articles and mushroom notes
8
7
 
9
8
  export default class extends Controller {
10
- static targets = [
11
- "input", "dropdown", "list", "hiddenIds", "loader"
12
- ]
13
- static values = {
14
- url: String,
15
- min: { type: Number, default: 4 },
16
- mushroomId: String,
17
- kind: String,
18
- requireUppercase: { type: Boolean, default: true }
19
- }
9
+ static targets = ["textarea", "dropdown"]
10
+ static values = {
11
+ genusUrl: String,
12
+ speciesUrl: String,
13
+ min: { type: Number, default: 4 }
14
+ }
20
15
 
21
- connect() {
22
- this.selected = new Map()
23
- this.debounceTimer = null
24
- this.inputTarget.addEventListener("input", this.onInput.bind(this))
25
- this.inputTarget.addEventListener("focus", this.onInput.bind(this))
26
- this.listTarget.querySelectorAll("[data-token-id]").forEach(el => {
27
- this.selected.set(el.dataset.tokenId, el.textContent.trim())
28
- })
29
- this.dropdownTarget.addEventListener("click", this.onDropdownClick.bind(this))
16
+ connect() {
17
+ this.debounceTimer = null
18
+ this.currentWord = ""
19
+ this.cursorPosition = 0
20
+ this.wordStart = 0
21
+ this.ignoreNextInput = false
22
+ }
23
+
24
+ onInput(event) {
25
+ // Skip if this input was triggered by our own text insertion
26
+ if (this.ignoreNextInput) {
27
+ this.ignoreNextInput = false
28
+ return
30
29
  }
31
30
 
32
- onInput() {
33
- clearTimeout(this.debounceTimer)
34
- let query = this.inputTarget.value.trim()
31
+ clearTimeout(this.debounceTimer)
35
32
 
36
- const minLength = parseInt(this.inputTarget.getAttribute("minlength")) || this.minValue
33
+ const textarea = this.textareaTarget
34
+ this.cursorPosition = textarea.selectionStart
35
+ const text = textarea.value
37
36
 
38
- // Check for uppercase requirement (genus names start with capital letter)
39
- if(this.requireUppercaseValue && query.length > 0 && query[0] !== query[0].toUpperCase()) {
40
- this.dropdownTarget.innerHTML = `<li class='px-3 py-2 text-sm text-gray-400'>Genus names start with uppercase (e.g., Agaricus)</li>`
41
- this.dropdownTarget.classList.remove("hidden")
42
- return
43
- }
37
+ // Get word at cursor
38
+ const wordInfo = this.getWordAtCursor(text, this.cursorPosition)
39
+ this.currentWord = wordInfo.word
40
+ this.wordStart = wordInfo.start
44
41
 
45
- // Show immediate feedback
46
- if(query.length > 0 && query.length < minLength) {
47
- this.dropdownTarget.innerHTML = `<li class='px-3 py-2 text-sm text-gray-400'>Type ${minLength - query.length} more character(s)...</li>`
48
- this.dropdownTarget.classList.remove("hidden")
49
- return
50
- }
42
+ // Check if should trigger autocomplete:
43
+ // 1. Uppercase word >= 4 chars (genus name like "Ganoderma")
44
+ // 2. OR lowercase word >= 4 chars AFTER a capitalized word (species epithet like "sessile" after "Ganoderma")
45
+ const isUppercase = /^[A-Z]/.test(this.currentWord)
46
+ const isLowercaseAfterGenus = /^[a-z]/.test(this.currentWord) && this.hasPrecedingCapitalizedWord(text, this.wordStart)
51
47
 
52
- if(query.length < minLength) {
53
- this.hideDropdown()
54
- return
55
- }
56
-
57
- // Show loading immediately before debounce
58
- this.showLoader()
59
- this.debounceTimer = setTimeout(() => {
60
- this.autocomplete(query)
61
- }, 150)
48
+ if (this.currentWord.length >= this.minValue && (isUppercase || isLowercaseAfterGenus)) {
49
+ this.debounceTimer = setTimeout(() => {
50
+ this.fetchSuggestions(this.currentWord)
51
+ }, 150)
52
+ } else {
53
+ this.hideDropdown()
62
54
  }
55
+ }
63
56
 
64
- autocomplete(query) {
65
- this.showLoader()
66
- const url = new URL(this.urlValue, window.location.origin)
67
- url.searchParams.append("q", query)
57
+ hasPrecedingCapitalizedWord(text, currentWordStart) {
58
+ // Look backwards from current word to find previous word
59
+ // Check if it starts with capital letter (likely a genus name)
60
+ let i = currentWordStart - 1
68
61
 
69
- // For species autocomplete, pass mushroom_id to filter by selected genera
70
- if(this.kindValue === "species" && this.hasMushroomIdValue) {
71
- url.searchParams.append("mushroom_id", this.mushroomIdValue)
72
- }
73
-
74
- fetch(url, { headers: {"Accept": "application/json"} })
75
- .then(r => r.json())
76
- .then(items => {
77
- this.renderDropdown(items)
78
- })
79
- .catch(_err => this.hideDropdown())
80
- .finally(() => this.hideLoader())
62
+ // Skip whitespace backwards
63
+ while (i >= 0 && /\s/.test(text[i])) {
64
+ i--
81
65
  }
82
66
 
83
- renderDropdown(items) {
84
- if(!items.length) {
85
- this.dropdownTarget.innerHTML = "<li class='px-3 py-2 text-sm text-gray-400'>No results</li>"
86
- this.dropdownTarget.classList.remove("hidden")
87
- return
88
- }
89
- this.dropdownTarget.innerHTML = items
90
- .map(item =>
91
- `<li class="hover:bg-blue-100 cursor-pointer px-3 py-2"
92
- data-id="${item.id}"
93
- data-label="${item.name}">
94
- ${item.name}
95
- </li>`
96
- ).join("")
97
- this.dropdownTarget.classList.remove("hidden")
67
+ if (i < 0) return false
68
+
69
+ // Find start of previous word
70
+ let prevWordEnd = i
71
+ while (i >= 0 && /[a-zA-Z]/.test(text[i])) {
72
+ i--
98
73
  }
99
74
 
100
- hideDropdown() {
101
- this.dropdownTarget.classList.add("hidden")
102
- this.dropdownTarget.innerHTML = ""
75
+ const prevWord = text.substring(i + 1, prevWordEnd + 1)
76
+ return prevWord.length > 0 && /^[A-Z]/.test(prevWord)
77
+ }
78
+
79
+ getPreviousWord(text, currentWordStart) {
80
+ // Extract the previous word (genus name) for filtering species
81
+ let i = currentWordStart - 1
82
+
83
+ // Skip whitespace backwards
84
+ while (i >= 0 && /\s/.test(text[i])) {
85
+ i--
103
86
  }
104
87
 
105
- showLoader() {
106
- if(this.hasLoaderTarget) this.loaderTarget.classList.remove("hidden")
88
+ if (i < 0) return ""
89
+
90
+ // Find start of previous word
91
+ let prevWordEnd = i
92
+ while (i >= 0 && /[a-zA-Z]/.test(text[i])) {
93
+ i--
107
94
  }
108
- hideLoader() {
109
- if(this.hasLoaderTarget) this.loaderTarget.classList.add("hidden")
95
+
96
+ return text.substring(i + 1, prevWordEnd + 1)
97
+ }
98
+
99
+ getWordAtCursor(text, position) {
100
+ // Find word boundaries (letters only, no spaces or punctuation)
101
+ let start = position
102
+ let end = position
103
+
104
+ // Move back to start of word
105
+ while (start > 0 && /[a-zA-Z]/.test(text[start - 1])) {
106
+ start--
110
107
  }
111
108
 
112
- onDropdownClick(e) {
113
- const li = e.target.closest("li[data-id]")
114
- if(!li) return
115
- const id = li.dataset.id
116
- const label = li.dataset.label
117
- if(this.selected.has(id)) {
118
- this.hideDropdown()
119
- this.inputTarget.value = ""
120
- return
121
- }
122
- this.createToken({id, label})
123
- this.hideDropdown()
124
- this.inputTarget.value = ""
125
- this.inputTarget.focus()
126
- // Save via AJAX
127
- this.saveToken(id, label)
109
+ // Move forward to end of word
110
+ while (end < text.length && /[a-zA-Z]/.test(text[end])) {
111
+ end++
128
112
  }
129
113
 
130
- createToken({id, label}) {
131
- if(this.selected.has(id)) return
132
- this.selected.set(id, label)
133
- // Create pill
134
- const pill = document.createElement("span")
135
- pill.className = "inline-flex items-center bg-gray-200 rounded px-2 py-0.5 text-sm mr-1 mb-1"
136
- pill.setAttribute("data-token-id", id)
137
- pill.innerHTML = `
138
- ${label}
139
- <button type="button" class="ml-1 text-gray-600 hover:text-red-600" aria-label="Remove" data-action="mycowriter--autocomplete#removeToken" data-id="${id}">&times;</button>
140
- `
141
- this.listTarget.appendChild(pill)
142
- this.updateHiddenIds()
114
+ return {
115
+ word: text.substring(start, end),
116
+ start: start,
117
+ end: end
143
118
  }
119
+ }
120
+
121
+ async fetchSuggestions(query) {
122
+ try {
123
+ const textarea = this.textareaTarget
124
+ const text = textarea.value
125
+ const isLowercaseAfterGenus = /^[a-z]/.test(query) && this.hasPrecedingCapitalizedWord(text, this.wordStart)
126
+
127
+ // If lowercase word after a capitalized word, search SPECIES first (likely typing species epithet)
128
+ if (isLowercaseAfterGenus) {
129
+ // Extract the previous genus name to filter species by that genus
130
+ const prevGenus = this.getPreviousWord(text, this.wordStart)
131
+
132
+ const speciesResponse = await fetch(
133
+ `${this.speciesUrlValue}?q=${encodeURIComponent(query)}&genus_name=${encodeURIComponent(prevGenus)}`,
134
+ { headers: { "Accept": "application/json" } }
135
+ )
144
136
 
145
- removeToken(e) {
146
- const id = e.target.dataset.id
147
- const pill = this.listTarget.querySelector(`[data-token-id="${id}"]`)
148
- const itemName = pill ? pill.textContent.trim().replace('×', '').trim() : 'this item'
137
+ if (speciesResponse.ok) {
138
+ const speciesData = await speciesResponse.json()
149
139
 
150
- // Show confirmation dialog
151
- if (!confirm(`Remove ${itemName}?`)) {
140
+ if (speciesData.length > 0) {
141
+ this.renderDropdown(speciesData)
152
142
  return
143
+ }
153
144
  }
154
145
 
155
- // Remove via AJAX first, then update UI on success
156
- this.deleteToken(id, () => {
157
- // Success callback
158
- this.selected.delete(id)
159
- if(pill) pill.remove()
160
- this.updateHiddenIds()
161
- }, () => {
162
- // Error callback - pill stays in place
146
+ // No species matches - try genus as fallback
147
+ const genusResponse = await fetch(`${this.genusUrlValue}?q=${encodeURIComponent(query)}`, {
148
+ headers: { "Accept": "application/json" }
163
149
  })
164
- }
165
-
166
- updateHiddenIds() {
167
- if(this.hasHiddenIdsTarget) {
168
- this.hiddenIdsTarget.value = Array.from(this.selected.keys()).join(",")
169
- }
170
- }
171
150
 
172
- // AJAX save for genus, species, tree, or plant association
173
- saveToken(id, label) {
174
- const kind = this.kindValue
175
- const mushroomId = this.mushroomIdValue
176
- if(!kind || !mushroomId) return
177
- let route, body
178
- if(kind=="genera") {
179
- route = `/genus_mushrooms.json`
180
- body = JSON.stringify({genus_mushroom: {mushroom_id: mushroomId, genus_id: id}})
181
- } else if (kind=="species") {
182
- route = `/mushroom_species.json`
183
- body = JSON.stringify({mushroom_species: {mushroom_id: mushroomId, species_id: id}})
184
- } else if (kind=="trees") {
185
- route = `/mushroom_trees.json`
186
- body = JSON.stringify({mushroom_tree: {mushroom_id: mushroomId, tree_id: id}})
187
- } else if (kind=="plants") {
188
- route = `/mushroom_plants.json`
189
- body = JSON.stringify({mushroom_plant: {mushroom_id: mushroomId, plant_id: id}})
151
+ if (genusResponse.ok) {
152
+ const genusData = await genusResponse.json()
153
+ if (genusData.length > 0) {
154
+ this.renderDropdown(genusData)
155
+ } else {
156
+ this.hideDropdown()
157
+ }
190
158
  } else {
191
- return
159
+ this.hideDropdown()
192
160
  }
193
- fetch(route, {
194
- method: "POST",
195
- headers: { "Content-Type": "application/json", "Accept": "application/json", "X-CSRF-Token": this.csrfToken() },
196
- body: body
197
- }).then(resp => {
198
- if(!resp.ok) {
199
- return resp.json().then(data => {
200
- throw new Error(data.errors ? data.errors.join(", ") : "Save failed")
201
- })
202
- }
203
- // Optionally: visual feedback
204
- }).catch(err => {
205
- // Remove pill if failed
206
- this.removePillById(id)
207
- alert(err.message || "Failed to add. Please try again.")
161
+ } else {
162
+ // Uppercase word - search GENUS ONLY (no species fallback)
163
+ // This prevents words like "Still" from matching species names with substring matches
164
+ const genusResponse = await fetch(`${this.genusUrlValue}?q=${encodeURIComponent(query)}`, {
165
+ headers: { "Accept": "application/json" }
208
166
  })
209
- }
210
167
 
211
- deleteToken(id, onSuccess, onError) {
212
- const kind = this.kindValue
213
- const mushroomId = this.mushroomIdValue
214
- if(!kind || !mushroomId) {
215
- if(onError) onError()
216
- return
217
- }
218
- let route
219
- if(kind=="genera") {
220
- route = `/genus_mushrooms/destroy_by_relation.json?mushroom_id=${mushroomId}&genus_id=${id}`
221
- } else if (kind=="species") {
222
- route = `/mushroom_species/destroy_by_relation.json?mushroom_id=${mushroomId}&species_id=${id}`
223
- } else if (kind=="trees") {
224
- route = `/mushroom_trees/destroy_by_relation.json?mushroom_id=${mushroomId}&tree_id=${id}`
225
- } else if (kind=="plants") {
226
- route = `/mushroom_plants/destroy_by_relation.json?mushroom_id=${mushroomId}&plant_id=${id}`
168
+ if (genusResponse.ok) {
169
+ const genusData = await genusResponse.json()
170
+
171
+ if (genusData.length > 0) {
172
+ this.renderDropdown(genusData)
173
+ } else {
174
+ this.hideDropdown()
175
+ }
227
176
  } else {
228
- if(onError) onError()
229
- return
177
+ this.hideDropdown()
230
178
  }
231
- fetch(route, {
232
- method: "DELETE",
233
- headers: {"Accept": "application/json", "X-CSRF-Token": this.csrfToken()}
234
- }).then(resp => {
235
- if(!resp.ok) {
236
- // Try to get the response text to see what's actually being returned
237
- return resp.text().then(text => {
238
- console.error('Delete failed. Status:', resp.status, 'Response:', text)
239
- try {
240
- const data = JSON.parse(text)
241
- throw new Error(data.message || `Delete failed (${resp.status})`)
242
- } catch(e) {
243
- throw new Error(`Delete failed (${resp.status}): ${text.substring(0, 100)}`)
244
- }
245
- })
246
- }
247
- return resp.json()
248
- }).then(_data => {
249
- // Success
250
- console.log('Delete successful')
251
- if(onSuccess) onSuccess()
252
- }).catch(err => {
253
- console.error('Delete error:', err)
254
- alert(err.message || "Failed to remove. Please try again.")
255
- if(onError) onError()
256
- })
179
+ }
180
+ } catch (error) {
181
+ console.error("Autocomplete error:", error)
182
+ this.hideDropdown()
257
183
  }
184
+ }
185
+
186
+ renderDropdown(items) {
187
+ this.dropdownTarget.innerHTML = items
188
+ .map(item => `
189
+ <li class="px-4 py-3 hover:bg-blue-500 hover:text-white cursor-pointer border-b border-gray-200 last:border-b-0"
190
+ data-action="click->inline-autocomplete#selectItem"
191
+ data-name="${item.name}">
192
+ <strong class="text-base">${item.name}</strong>
193
+ </li>
194
+ `).join("")
195
+
196
+ this.dropdownTarget.classList.remove("hidden")
197
+ }
258
198
 
259
- removePillById(id) {
260
- const pill = this.listTarget.querySelector(`[data-token-id="${id}"]`)
261
- if(pill) pill.remove()
262
- this.selected.delete(id)
263
- this.updateHiddenIds()
199
+ selectItem(event) {
200
+ event.preventDefault()
201
+ event.stopPropagation()
202
+
203
+ const selectedName = event.currentTarget.dataset.name
204
+ const textarea = this.textareaTarget
205
+ const text = textarea.value
206
+
207
+ // Check if we're replacing a species epithet (lowercase after genus)
208
+ // If so, we need to replace BOTH the genus and species words to avoid duplication
209
+ const isLowercaseWord = /^[a-z]/.test(this.currentWord)
210
+ const hasPrevGenus = this.hasPrecedingCapitalizedWord(text, this.wordStart)
211
+
212
+ let before, after, replaceStart
213
+ if (isLowercaseWord && hasPrevGenus) {
214
+ // Find start of previous genus word
215
+ const prevGenus = this.getPreviousWord(text, this.wordStart)
216
+ const genusStart = this.wordStart - prevGenus.length - 1 // -1 for space
217
+
218
+ replaceStart = genusStart
219
+ before = text.substring(0, genusStart)
220
+ after = text.substring(this.cursorPosition)
221
+ } else {
222
+ // Normal replacement - just replace current word
223
+ replaceStart = this.wordStart
224
+ before = text.substring(0, this.wordStart)
225
+ after = text.substring(this.cursorPosition)
226
+ }
227
+
228
+ // Apply <em> tags to complete binomials (contains space like "Ganoderma sessile")
229
+ // This follows scientific naming conventions: binomial names must be italicized
230
+ const isBinomial = selectedName.includes(' ')
231
+
232
+ let formattedName, cursorOffset
233
+ if (isBinomial) {
234
+ // Complete binomial - wrap in <em> tags per nomenclature standards
235
+ formattedName = `<em>${selectedName}</em>`
236
+ cursorOffset = selectedName.length + 9 // position AFTER closing </em> tag
237
+ } else {
238
+ // Genus only - no HTML tags, add trailing space
239
+ formattedName = selectedName + ' '
240
+ cursorOffset = selectedName.length + 1
264
241
  }
265
242
 
266
- csrfToken() {
267
- const meta = document.querySelector('meta[name=csrf-token]')
268
- return meta && meta.content
243
+ textarea.value = before + formattedName + after
244
+
245
+ // Hide dropdown immediately
246
+ this.hideDropdown()
247
+
248
+ // Position cursor after the insertion
249
+ const newPosition = before.length + cursorOffset
250
+ textarea.setSelectionRange(newPosition, newPosition)
251
+ textarea.focus()
252
+
253
+ // Set flag to ignore the input event we're about to trigger
254
+ this.ignoreNextInput = true
255
+
256
+ // Trigger input event for character count update
257
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
258
+ }
259
+
260
+ hideDropdown() {
261
+ this.dropdownTarget.classList.add("hidden")
262
+ this.dropdownTarget.innerHTML = ""
263
+ }
264
+
265
+ onKeydown(event) {
266
+ // Handle keyboard navigation in dropdown
267
+ if (!this.dropdownTarget.classList.contains("hidden")) {
268
+ if (event.key === "Escape") {
269
+ this.hideDropdown()
270
+ event.preventDefault()
271
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
272
+ // TODO: Add arrow key navigation through dropdown items
273
+ event.preventDefault()
274
+ }
269
275
  }
276
+ }
270
277
  }
@@ -4,36 +4,36 @@ Mycowriter has been installed successfully!
4
4
 
5
5
  Next steps:
6
6
 
7
- 1. Run database migrations if you haven't already:
8
- rails db:migrate
7
+ 1. Restart your Rails server to load the new Stimulus controller
9
8
 
10
9
  2. Ensure you have the mb_lists table in your database with taxonomic data.
11
10
  The gem expects a table with at least these columns:
12
11
  - id
13
12
  - taxon_name
14
13
  - rank_name (optional, for filtering by taxonomic rank)
14
+ - name_status (optional, recommended: filters to 'Legitimate' names only)
15
15
 
16
- 3. Import your controller in app/javascript/controllers/application.js:
16
+ 3. Use in your text fields (articles, descriptions, notes):
17
17
 
18
- import MycowriterAutocomplete from "mycowriter/autocomplete_controller"
19
- application.register("mycowriter--autocomplete", MycowriterAutocomplete)
18
+ <div data-controller="inline-autocomplete"
19
+ data-inline-autocomplete-genus-url-value="<%= mycowriter.genera_autocomplete_path %>"
20
+ data-inline-autocomplete-species-url-value="<%= mycowriter.species_autocomplete_path %>"
21
+ data-inline-autocomplete-min-value="4">
20
22
 
21
- 4. Use the autocomplete in your views:
23
+ <%= f.text_area :body,
24
+ data: {
25
+ inline_autocomplete_target: "textarea",
26
+ action: "input->inline-autocomplete#onInput keydown->inline-autocomplete#onKeydown"
27
+ },
28
+ class: "form-textarea" %>
22
29
 
23
- <div data-controller="mycowriter--autocomplete"
24
- data-mycowriter--autocomplete-url-value="/mycowriter/autocomplete/genera"
25
- data-mycowriter--autocomplete-kind-value="genera"
26
- data-mycowriter--autocomplete-mushroom-id-value="<%= @mushroom.id %>">
27
-
28
- <input type="text"
29
- data-mycowriter--autocomplete-target="input"
30
- placeholder="Type genus name..." />
31
-
32
- <ul data-mycowriter--autocomplete-target="dropdown" class="hidden"></ul>
33
- <div data-mycowriter--autocomplete-target="list"></div>
30
+ <div data-inline-autocomplete-target="dropdown" class="hidden"></div>
34
31
  </div>
35
32
 
36
- 5. Configure Mycowriter in config/initializers/mycowriter.rb
33
+ When typing with a capital letter + 3 chars, genus suggestions appear.
34
+ Select a genus, then continue typing species for inline completion.
35
+
36
+ 4. Configure Mycowriter in config/initializers/mycowriter.rb
37
37
 
38
38
  For more information, visit https://mycowriter.com
39
39
 
@@ -11,6 +11,10 @@ module Mycowriter
11
11
  template "mycowriter.rb", "config/initializers/mycowriter.rb"
12
12
  end
13
13
 
14
+ def copy_javascript
15
+ template "autocomplete_controller.js", "app/javascript/controllers/inline_autocomplete_controller.js"
16
+ end
17
+
14
18
  def add_route
15
19
  route 'mount Mycowriter::Engine => "/mycowriter"'
16
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mycowriter
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.10"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mycowriter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will Johnston