From fab336cd69457d092d2f5fb32fb09f7c49ea232e Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Sun, 20 Dec 2020 15:14:07 -0800 Subject: [PATCH 01/16] Introducing Autocomplete for Meteor AutoForm - Added new inputTypes autocomplete directory - Added HTML for Autocomplete Template - Added JS for Autocomplete Template (WIP: Need actions!) - Styles will go inside `meteor-autoform-themes` project --- dynamic.js | 2 ++ inputTypes/autocomplete/autocomplete.html | 3 +++ inputTypes/autocomplete/autocomplete.js | 21 +++++++++++++++++++++ static.js | 2 ++ 4 files changed, 28 insertions(+) create mode 100644 inputTypes/autocomplete/autocomplete.html create mode 100644 inputTypes/autocomplete/autocomplete.js diff --git a/dynamic.js b/dynamic.js index d9bde7a5..d9d227de 100644 --- a/dynamic.js +++ b/dynamic.js @@ -23,6 +23,8 @@ function init () { import('./formTypes/disabled.js'), // input types import('./inputTypes/value-converters.js'), + import('./inputTypes/autocomplete/autocomplete.html'), + import('./inputTypes/autocomplete/autocomplete.js'), import('./inputTypes/boolean-checkbox/boolean-checkbox.html'), import('./inputTypes/boolean-checkbox/boolean-checkbox.js'), import('./inputTypes/boolean-radios/boolean-radios.html'), diff --git a/inputTypes/autocomplete/autocomplete.html b/inputTypes/autocomplete/autocomplete.html new file mode 100644 index 00000000..1425d7b7 --- /dev/null +++ b/inputTypes/autocomplete/autocomplete.html @@ -0,0 +1,3 @@ + diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js new file mode 100644 index 00000000..4e49858d --- /dev/null +++ b/inputTypes/autocomplete/autocomplete.js @@ -0,0 +1,21 @@ +import { Template } from 'meteor/templating' +console.log("ADDING AUTOCOMPLETE") +AutoForm.addInputType('autocomplete', { + template: 'afAutocomplete', + valueOut: function () { + return this.val() + }, + valueConverters: { + stringArray: AutoForm.valueConverters.stringToStringArray, + number: AutoForm.valueConverters.stringToNumber, + numberArray: AutoForm.valueConverters.stringToNumberArray, + boolean: AutoForm.valueConverters.stringToBoolean, + booleanArray: AutoForm.valueConverters.stringToBooleanArray, + date: AutoForm.valueConverters.stringToDate, + dateArray: AutoForm.valueConverters.stringToDateArray + }, + contextAdjust: function (context) { + context.atts.autocomplete = 'off' + return context + } +}) diff --git a/static.js b/static.js index 2d42755f..eec728c0 100644 --- a/static.js +++ b/static.js @@ -10,6 +10,8 @@ import './formTypes/readonly.js' import './formTypes/disabled.js' // input types import './inputTypes/value-converters.js' +import './inputTypes/autocomplete/autocomplete.html' +import './inputTypes/autocomplete/autocomplete.js' import './inputTypes/boolean-checkbox/boolean-checkbox.html' import './inputTypes/boolean-checkbox/boolean-checkbox.js' import './inputTypes/boolean-radios/boolean-radios.html' From a06e00b99e4db14cad70a932e58ca99a7bb8bef2 Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Wed, 23 Dec 2020 22:09:55 -0800 Subject: [PATCH 02/16] Meteor AutoForm 7.1.0 - Native Autocomplete - Circular selection - Reactive options - Simpl-Schema defined options - Navigable via arrows - No third party solutions --- autoform-helpers.js | 21 +++ inputTypes/autocomplete/autocomplete.html | 7 +- inputTypes/autocomplete/autocomplete.js | 182 +++++++++++++++++++++- package.js | 2 +- 4 files changed, 209 insertions(+), 3 deletions(-) diff --git a/autoform-helpers.js b/autoform-helpers.js index c7d5577c..25f0791a 100644 --- a/autoform-helpers.js +++ b/autoform-helpers.js @@ -425,6 +425,27 @@ export const afSelectOptionAtts = function afSelectOptionAtts () { */ Template.registerHelper('afSelectOptionAtts', afSelectOptionAtts) +/** + * @name afAutocompleteSuggestionAtts + * @return {*} + */ +export const afAutocompleteSuggestionAtts = function afAutocompleteSuggestionAtts () { + if (this.value === false) this.value = 'false' + const atts = 'value' in this ? { value: this.value } : {} + if (this.selected) { + atts.selected = '' + } + if (this.htmlAtts) { + Object.assign(atts, this.htmlAtts) + } + return atts +} + +/* + * afAutocompleteSuggetionAtts + */ +Template.registerHelper('afAutocompleteSuggestionAtts', afAutocompleteSuggestionAtts) + // Expects to be called with this.name available Template.registerHelper('afOptionsFromSchema', function afOptionsFromSchema () { return AutoForm._getOptionsForField(this.name) diff --git a/inputTypes/autocomplete/autocomplete.html b/inputTypes/autocomplete/autocomplete.html index 1425d7b7..27578db2 100644 --- a/inputTypes/autocomplete/autocomplete.html +++ b/inputTypes/autocomplete/autocomplete.html @@ -1,3 +1,8 @@ diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 4e49858d..ca92865e 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -1,5 +1,4 @@ import { Template } from 'meteor/templating' -console.log("ADDING AUTOCOMPLETE") AutoForm.addInputType('autocomplete', { template: 'afAutocomplete', valueOut: function () { @@ -16,6 +15,187 @@ AutoForm.addInputType('autocomplete', { }, contextAdjust: function (context) { context.atts.autocomplete = 'off' + const itemAtts = { ...context.atts } + + // build items list + context.items = [] + + // re-use selectOptions to keep it DRY + // Add all defined options + context.selectOptions.forEach(function(opt) { + // there are no subgroups here + const { label, value, ...htmlAtts } = opt + context.items.push({ + name: context.name, + label, + value, + htmlAtts, + _id: opt.value.toString(), + selected: (opt.value === context.value), + atts: itemAtts + }) + }) + return context } }) + +Template.afAutocomplete.onRendered(function() { + /* AUTOCOMPLETE + *************** + * This uses the same datums as select types, which + * means that 'options' come from simple-schema. + * + * It allows selection by arrows up/down/enter; mouse click; + * and when enough characters entered make a positive match. + * Arrow nanigation is circlular; top to bottom & vice versa. + * + * It needs some class definitions and styles, such as: + * + * .autocomplete { + * .ac-container { // trick to prevent calculating position + * position: relative; + * height: 0; + * + * .ac-suggestions { + * position: absolute; + * top: 0; + * left: 0; + * width: 100%; + * z-index: $zindex-modal + 1; + * border-radius: $border-radius-lg; + * background-color: lighten($card-black-background, 6%); + * overflow: hidden; + * + * > div { + * padding: 15px; + * font-size: $font-size-sm; + * + * &:first-child { + * border-color: lighten($nav-gray,5%); + * border-top: 2px solid; + * } + * + * &:hover, + * &.ac-selected { + * background-color: darken($white, 10%); + * color: $black; + * } + * } + * } + * } + * } + */ + + // get the instance items + // defined in several ways + const me = Template.instance() + const items = me.data.items + + // secure the dom so multiple autocompletes don't clash + const $me = $(me.firstNode) + const $input = $me.children('input') + const $container = $me.children('.ac-container') + const $suggestions = $container.children('.ac-suggestions') + + // prepare for arrow navigation + let currIndex = -1 + let totalItems = 0 + + // prevent form submit from "Enter/Return" + $input.keypress((e) => + { + if (e.originalEvent.key === "Enter") + { + e.preventDefault() + e.stopPropagation() + } + }) + + // detect the keystrokes + $input.keyup((e) => + { + // only populate when typing characters or deleting + // otherwise, we are navigating + if (/ArrowDown|ArrowUp|Enter/.test(e.originalEvent.key) === false) + { + // we're typing + // filter results from input + let result = me.data.items.filter((i) => { + let reg = new RegExp(e.target.value, 'gi') + return reg.test(i.value) + }) + + // display results in 'suggestions' div + $suggestions.empty() + let len = result.length + totalItems = result.length + if (len > 1) + { + while (--len > -1) + { + // populate suggestions + let html = `
${result[len].label}` + $suggestions.append(html) + } + + // clear any manual navigated selections on hover + $suggestions.children().hover((e) => { + $suggestions.children().removeClass('ac-selected') + currIndex = -1 + }) + + // choose an answer on click + $suggestions.children().click((e) => { + let dataValue = $(e.target).attr('data-value') + $input.val(dataValue) + $suggestions.empty() + }) + } + else if (e.originalEvent.key !== "Backspace") + { + // only force populate if not deleting + // bc we all make mistakes + if (result.length === 1) + { + $input.val(result[0].value) + } + } + } + else // we're navigating suggestions + { + // start highlighting at the 0 index + if (/ArrowDown/.test(e.originalEvent.key) === true) + { + // navigating down + if (currIndex === totalItems - 1) + { + currIndex = -1 + } + // remove all classes from the children + $suggestions.children().removeClass('ac-selected') + $suggestions.children('div').eq(++currIndex).addClass('ac-selected') + } + else if (/ArrowUp/.test(e.originalEvent.key) === true) + { + if (currIndex <= 0) + { + currIndex = totalItems + } + // navigating up + // remove all classes from the children + $suggestions.children().removeClass('ac-selected') + $suggestions.children('div').eq(--currIndex).addClass('ac-selected') + } + else if (/Enter/.test(e.originalEvent.key) === true) + { + // we're selecting + let enterVal = $suggestions.children('div').eq(currIndex).attr('data-value') + $input.val(enterVal) + $suggestions.empty() + currIndex = -1 + totalItems = 0 + } + } + }) +}) diff --git a/package.js b/package.js index d0d404a6..0209b5b3 100644 --- a/package.js +++ b/package.js @@ -4,7 +4,7 @@ Package.describe({ summary: 'Easily create forms with automatic insert and update, and automatic reactive validation.', git: 'https://github.com/aldeed/meteor-autoform.git', - version: '7.0.0' + version: '7.1.0' }) Package.onUse(function (api) { From b9da3c73485378ca1a94cb54d07ab9738f47f32f Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Tue, 29 Dec 2020 01:47:53 -0800 Subject: [PATCH 03/16] Meteor AutoForm v7.1.0 - Autocomplete - Uses bootstrap 4 dropdown class definitions for style - Conforms to StandardJS delinting and format --- inputTypes/autocomplete/autocomplete.html | 6 +- inputTypes/autocomplete/autocomplete.js | 162 +++++++++------------- 2 files changed, 66 insertions(+), 102 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.html b/inputTypes/autocomplete/autocomplete.html index 27578db2..15eafc8f 100644 --- a/inputTypes/autocomplete/autocomplete.html +++ b/inputTypes/autocomplete/autocomplete.html @@ -1,8 +1,6 @@ diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index ca92865e..7c7d4e31 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -1,4 +1,5 @@ import { Template } from 'meteor/templating' + AutoForm.addInputType('autocomplete', { template: 'afAutocomplete', valueOut: function () { @@ -21,26 +22,30 @@ AutoForm.addInputType('autocomplete', { context.items = [] // re-use selectOptions to keep it DRY - // Add all defined options - context.selectOptions.forEach(function(opt) { - // there are no subgroups here - const { label, value, ...htmlAtts } = opt - context.items.push({ - name: context.name, - label, - value, - htmlAtts, - _id: opt.value.toString(), - selected: (opt.value === context.value), - atts: itemAtts + // Add all defined options or default + if (context.selectOptions) { + context.selectOptions.forEach(function (opt) { + // there are no subgroups here + const { label, value, ...htmlAtts } = opt + context.items.push({ + name: context.name, + label, + value, + htmlAtts, + _id: opt.value.toString(), + selected: (opt.value === context.value), + atts: itemAtts + }) }) - }) + } else { + console.warn('autocomplete requires options for suggestions.') + } return context } }) -Template.afAutocomplete.onRendered(function() { +Template.afAutocomplete.onRendered(function () { /* AUTOCOMPLETE *************** * This uses the same datums as select types, which @@ -48,151 +53,112 @@ Template.afAutocomplete.onRendered(function() { * * It allows selection by arrows up/down/enter; mouse click; * and when enough characters entered make a positive match. - * Arrow nanigation is circlular; top to bottom & vice versa. - * - * It needs some class definitions and styles, such as: - * - * .autocomplete { - * .ac-container { // trick to prevent calculating position - * position: relative; - * height: 0; - * - * .ac-suggestions { - * position: absolute; - * top: 0; - * left: 0; - * width: 100%; - * z-index: $zindex-modal + 1; - * border-radius: $border-radius-lg; - * background-color: lighten($card-black-background, 6%); - * overflow: hidden; - * - * > div { - * padding: 15px; - * font-size: $font-size-sm; + * Arrow navigation is circlular; top to bottom & vice versa. * - * &:first-child { - * border-color: lighten($nav-gray,5%); - * border-top: 2px solid; - * } - * - * &:hover, - * &.ac-selected { - * background-color: darken($white, 10%); - * color: $black; - * } - * } - * } - * } - * } + * It uses the 'dropdown' classes in bootstrap 4 for styling. */ // get the instance items // defined in several ways const me = Template.instance() - const items = me.data.items // secure the dom so multiple autocompletes don't clash - const $me = $(me.firstNode) - const $input = $me.children('input') - const $container = $me.children('.ac-container') - const $suggestions = $container.children('.ac-suggestions') + const $input = me.$('input') + const $container = me.$('.dropdown') + const $suggestions = me.$('.dropdown-menu') // prepare for arrow navigation let currIndex = -1 let totalItems = 0 // prevent form submit from "Enter/Return" - $input.keypress((e) => - { - if (e.originalEvent.key === "Enter") - { + $input.keypress((e) => { + if (e.originalEvent.key === 'Enter') { e.preventDefault() e.stopPropagation() } }) // detect the keystrokes - $input.keyup((e) => - { + $input.keyup((e) => { // only populate when typing characters or deleting // otherwise, we are navigating - if (/ArrowDown|ArrowUp|Enter/.test(e.originalEvent.key) === false) - { + if (/ArrowDown|ArrowUp|Enter/.test(e.originalEvent.key) === false) { // we're typing // filter results from input - let result = me.data.items.filter((i) => { - let reg = new RegExp(e.target.value, 'gi') - return reg.test(i.value) + const result = me.data.items.filter((i) => { + const reg = new RegExp(e.target.value, 'gi') + return reg.test(i.label) }) // display results in 'suggestions' div $suggestions.empty() + let html let len = result.length totalItems = result.length - if (len > 1) - { - while (--len > -1) - { + + if (len > 1) { + currIndex = -1 + while (--len > -1) { // populate suggestions - let html = `
${result[len].label}` + html = `` $suggestions.append(html) + $suggestions.addClass('show') + $container.addClass('show') } // clear any manual navigated selections on hover $suggestions.children().hover((e) => { - $suggestions.children().removeClass('ac-selected') + $suggestions.children().removeClass('active') currIndex = -1 }) // choose an answer on click $suggestions.children().click((e) => { - let dataValue = $(e.target).attr('data-value') + const dataValue = $(e.target).attr('data-value') $input.val(dataValue) $suggestions.empty() + $suggestions.removeClass('show') + $container.removeClass('show') }) - } - else if (e.originalEvent.key !== "Backspace") - { + } else if (e.originalEvent.key !== 'Backspace') { // only force populate if not deleting // bc we all make mistakes - if (result.length === 1) - { + if (result.length === 1) { $input.val(result[0].value) + $suggestions.removeClass('show') + $container.removeClass('show') + } else { + // no results, hide + $suggestions.removeClass('show') + $container.removeClass('show') } } - } - else // we're navigating suggestions - { + } else { // we're navigating suggestions // start highlighting at the 0 index - if (/ArrowDown/.test(e.originalEvent.key) === true) - { + if (/ArrowDown/.test(e.originalEvent.key) === true) { // navigating down - if (currIndex === totalItems - 1) - { + if (currIndex === totalItems - 1) { currIndex = -1 } // remove all classes from the children - $suggestions.children().removeClass('ac-selected') - $suggestions.children('div').eq(++currIndex).addClass('ac-selected') - } - else if (/ArrowUp/.test(e.originalEvent.key) === true) - { - if (currIndex <= 0) - { + $suggestions.children().removeClass('active') + $suggestions.children('div').eq(++currIndex).addClass('active') + } else if (/ArrowUp/.test(e.originalEvent.key) === true) { + if (currIndex <= 0) { currIndex = totalItems } // navigating up // remove all classes from the children - $suggestions.children().removeClass('ac-selected') - $suggestions.children('div').eq(--currIndex).addClass('ac-selected') - } - else if (/Enter/.test(e.originalEvent.key) === true) - { + $suggestions.children().removeClass('active') + $suggestions.children('div').eq(--currIndex).addClass('active') + } else if (/Enter/.test(e.originalEvent.key) === true) { // we're selecting - let enterVal = $suggestions.children('div').eq(currIndex).attr('data-value') + const enterVal = $suggestions.children('div').eq(currIndex).attr('data-value') $input.val(enterVal) $suggestions.empty() + $suggestions.removeClass('show') + $container.removeClass('show') currIndex = -1 totalItems = 0 } From 3538e8578b577f567e5e68205c70a9a9a687d8ef Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Wed, 30 Dec 2020 11:49:18 -0800 Subject: [PATCH 04/16] Meteor AutoForm v7.1.0 - Autocomplete - added hidden input field to allow labels and values in autocomplete; think 'Full Name' and 'UserId' - now adjusting context for visible and hidden inputs, so AutoForm works as expected on submit - run lint passed - run test passed --- inputTypes/autocomplete/autocomplete.html | 3 +- inputTypes/autocomplete/autocomplete.js | 61 +++++++++++++++++------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.html b/inputTypes/autocomplete/autocomplete.html index 15eafc8f..9d83ff6a 100644 --- a/inputTypes/autocomplete/autocomplete.html +++ b/inputTypes/autocomplete/autocomplete.html @@ -1,6 +1,7 @@ diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 7c7d4e31..6d3a834c 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -17,7 +17,14 @@ AutoForm.addInputType('autocomplete', { contextAdjust: function (context) { context.atts.autocomplete = 'off' const itemAtts = { ...context.atts } - + // remove non-essential atts from visible input + const visibleAtts = Object.assign({}, { ...context.atts }) + const keys = ['data-schema-key', 'id', 'name'] + keys.forEach(key => { + delete visibleAtts[key] + }) + // add form-control to remaining classes + context.visibleAtts = AutoForm.Utility.addClass({ ...visibleAtts }, 'form-control') // build items list context.items = [] @@ -37,14 +44,25 @@ AutoForm.addInputType('autocomplete', { atts: itemAtts }) }) - } else { + } + else { console.warn('autocomplete requires options for suggestions.') } - return context } }) +Template.afAutocomplete.helpers({ + visibleAtts () { + console.log(this.atts) + const atts = this.atts + return atts + }, + hiddenAtts () { + return this.atts + } +}) + Template.afAutocomplete.onRendered(function () { /* AUTOCOMPLETE *************** @@ -63,7 +81,8 @@ Template.afAutocomplete.onRendered(function () { const me = Template.instance() // secure the dom so multiple autocompletes don't clash - const $input = me.$('input') + const $input = me.$('input[type="text"]') + const $hidden = me.$('input[type="hidden"]') const $container = me.$('.dropdown') const $suggestions = me.$('.dropdown-menu') @@ -85,7 +104,7 @@ Template.afAutocomplete.onRendered(function () { // otherwise, we are navigating if (/ArrowDown|ArrowUp|Enter/.test(e.originalEvent.key) === false) { // we're typing - // filter results from input + // filter results from visible input value const result = me.data.items.filter((i) => { const reg = new RegExp(e.target.value, 'gi') return reg.test(i.label) @@ -101,7 +120,7 @@ Template.afAutocomplete.onRendered(function () { currIndex = -1 while (--len > -1) { // populate suggestions - html = `` + html = `` $suggestions.append(html) $suggestions.addClass('show') $container.addClass('show') @@ -115,26 +134,32 @@ Template.afAutocomplete.onRendered(function () { // choose an answer on click $suggestions.children().click((e) => { - const dataValue = $(e.target).attr('data-value') - $input.val(dataValue) + const dataValue = me.$(e.target).attr('data-value') + const dataLabel = me.$(e.target).attr('data-label') + $input.val(dataLabel) + $hidden.val(dataValue) $suggestions.empty() $suggestions.removeClass('show') $container.removeClass('show') }) - } else if (e.originalEvent.key !== 'Backspace') { + } + else if (e.originalEvent.key !== 'Backspace') { // only force populate if not deleting // bc we all make mistakes if (result.length === 1) { - $input.val(result[0].value) + $input.val(result[0].label) + $hidden.val(result[0].value) $suggestions.removeClass('show') $container.removeClass('show') - } else { + } + else { // no results, hide $suggestions.removeClass('show') $container.removeClass('show') } } - } else { // we're navigating suggestions + } + else { // we're navigating suggestions // start highlighting at the 0 index if (/ArrowDown/.test(e.originalEvent.key) === true) { // navigating down @@ -144,7 +169,8 @@ Template.afAutocomplete.onRendered(function () { // remove all classes from the children $suggestions.children().removeClass('active') $suggestions.children('div').eq(++currIndex).addClass('active') - } else if (/ArrowUp/.test(e.originalEvent.key) === true) { + } + else if (/ArrowUp/.test(e.originalEvent.key) === true) { if (currIndex <= 0) { currIndex = totalItems } @@ -152,10 +178,13 @@ Template.afAutocomplete.onRendered(function () { // remove all classes from the children $suggestions.children().removeClass('active') $suggestions.children('div').eq(--currIndex).addClass('active') - } else if (/Enter/.test(e.originalEvent.key) === true) { + } + else if (/Enter/.test(e.originalEvent.key) === true) { // we're selecting - const enterVal = $suggestions.children('div').eq(currIndex).attr('data-value') - $input.val(enterVal) + const enterValue = $suggestions.children('div').eq(currIndex).attr('data-value') + const enterLabel = $suggestions.children('div').eq(currIndex).attr('data-label') + $input.val(enterLabel) + $hidden.val(enterValue) $suggestions.empty() $suggestions.removeClass('show') $container.removeClass('show') From a3019f4a603f8d860e07b36f0f76a09128510bb7 Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Wed, 30 Dec 2020 21:43:07 -0800 Subject: [PATCH 05/16] Meteor AutoForm v7.1.0 - Autocomplete - Escape closes the dropdown - Double-click opens the dropdown - Removed a lone helpers definition of no value - Preserved sort order of backing object array - Form /should/ submit properly when 'Enter' triggers and dropdown is hidden - Maybe we're all clear for launch, but I'm not counting my chickens yet --- inputTypes/autocomplete/autocomplete.js | 95 ++++++++++++++++--------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 6d3a834c..b40d197d 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -52,17 +52,6 @@ AutoForm.addInputType('autocomplete', { } }) -Template.afAutocomplete.helpers({ - visibleAtts () { - console.log(this.atts) - const atts = this.atts - return atts - }, - hiddenAtts () { - return this.atts - } -}) - Template.afAutocomplete.onRendered(function () { /* AUTOCOMPLETE *************** @@ -89,20 +78,50 @@ Template.afAutocomplete.onRendered(function () { // prepare for arrow navigation let currIndex = -1 let totalItems = 0 + let showing = false - // prevent form submit from "Enter/Return" - $input.keypress((e) => { - if (e.originalEvent.key === 'Enter') { + const clearDropdown = function (e, haltEvents = false) { + if (showing === true) { + // hide the menu and reset the params + $suggestions.empty().removeClass('show') + $container.removeClass('show') + currIndex = -1 + totalItems = 0 + showing = false + if (haltEvents === true) { + e.preventDefault() + e.stopPropagation() + } + } + } + + // keydown catches escape + $input.keydown((e) => { + // prevent form submit from "Enter/Return" if showing + if ( + /Enter/.test(e.originalEvent.key) === true && + showing === true + ) { e.preventDefault() e.stopPropagation() } + // allow Escape to close the dropdown + else if ( + /Escape/.test(e.originalEvent.key) === true && + showing === true + ) { + clearDropdown(e, true) + } }) - // detect the keystrokes - $input.keyup((e) => { + // clear on blur? + // TODO: Figure out how blur won't block "click" + // $input.blur((e)=>{ clearDropdown(e) }) + + const callback = function (e) { // only populate when typing characters or deleting // otherwise, we are navigating - if (/ArrowDown|ArrowUp|Enter/.test(e.originalEvent.key) === false) { + if (/ArrowDown|ArrowUp|ArrowLeft|ArrowRight|Enter|Escape/.test(e.originalEvent.key) === false) { // we're typing // filter results from visible input value const result = me.data.items.filter((i) => { @@ -113,18 +132,19 @@ Template.afAutocomplete.onRendered(function () { // display results in 'suggestions' div $suggestions.empty() let html - let len = result.length + const len = result.length totalItems = result.length if (len > 1) { currIndex = -1 - while (--len > -1) { + for (let i = 0; i < len; i++) { // populate suggestions - html = `` + html = `` $suggestions.append(html) - $suggestions.addClass('show') - $container.addClass('show') } + $suggestions.addClass('show') + $container.addClass('show') + showing = true // clear any manual navigated selections on hover $suggestions.children().hover((e) => { @@ -138,9 +158,7 @@ Template.afAutocomplete.onRendered(function () { const dataLabel = me.$(e.target).attr('data-label') $input.val(dataLabel) $hidden.val(dataValue) - $suggestions.empty() - $suggestions.removeClass('show') - $container.removeClass('show') + clearDropdown(e, false) }) } else if (e.originalEvent.key !== 'Backspace') { @@ -149,17 +167,15 @@ Template.afAutocomplete.onRendered(function () { if (result.length === 1) { $input.val(result[0].label) $hidden.val(result[0].value) - $suggestions.removeClass('show') - $container.removeClass('show') + clearDropdown(e, false) } else { // no results, hide - $suggestions.removeClass('show') - $container.removeClass('show') + clearDropdown(e, false) } } } - else { // we're navigating suggestions + else if (showing === true) { // we're navigating suggestions // start highlighting at the 0 index if (/ArrowDown/.test(e.originalEvent.key) === true) { // navigating down @@ -181,16 +197,25 @@ Template.afAutocomplete.onRendered(function () { } else if (/Enter/.test(e.originalEvent.key) === true) { // we're selecting + if (currIndex === -1) { + currIndex = 0 + } const enterValue = $suggestions.children('div').eq(currIndex).attr('data-value') const enterLabel = $suggestions.children('div').eq(currIndex).attr('data-label') $input.val(enterLabel) $hidden.val(enterValue) - $suggestions.empty() - $suggestions.removeClass('show') - $container.removeClass('show') - currIndex = -1 - totalItems = 0 + clearDropdown(e, false) } } + } + + // detect keystrokes + $input.keyup((e) => { + callback(e) + }) + + // show on double click + $input.dblclick((e) => { + callback(e) }) }) From 2f020e6266d8d8acde01955b0dec3d1d77e722ce Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Mon, 4 Jan 2021 23:34:07 -0800 Subject: [PATCH 06/16] Meteor AutoForm v7.1.0 - Autocomplete - Fixed HTML structure to display bootstrap validation errors properly in autocomplete inputs. --- inputTypes/autocomplete/autocomplete.html | 2 +- inputTypes/autocomplete/autocomplete.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/inputTypes/autocomplete/autocomplete.html b/inputTypes/autocomplete/autocomplete.html index 9d83ff6a..4d352d13 100644 --- a/inputTypes/autocomplete/autocomplete.html +++ b/inputTypes/autocomplete/autocomplete.html @@ -1,7 +1,7 @@ diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index b40d197d..227240ba 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -25,6 +25,7 @@ AutoForm.addInputType('autocomplete', { }) // add form-control to remaining classes context.visibleAtts = AutoForm.Utility.addClass({ ...visibleAtts }, 'form-control') + context.atts = AutoForm.Utility.addClass({ ...itemAtts }, 'form-control') // build items list context.items = [] From b18f721761e455512abfebdd2d8293f6b16ae8b2 Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Tue, 5 Jan 2021 19:31:58 -0800 Subject: [PATCH 07/16] Meteor AutoForm v7.1.0 - Autocomplete - Now the values between the hidden and visible fields remain identical so that validation works correctly if the value is deleted. --- inputTypes/autocomplete/autocomplete.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 227240ba..67e51980 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -124,6 +124,8 @@ Template.afAutocomplete.onRendered(function () { // otherwise, we are navigating if (/ArrowDown|ArrowUp|ArrowLeft|ArrowRight|Enter|Escape/.test(e.originalEvent.key) === false) { // we're typing + // ensure hidden and visible values match for validation + $hidden.val($input.val()) // filter results from visible input value const result = me.data.items.filter((i) => { const reg = new RegExp(e.target.value, 'gi') From 5930cc333e924445772e1d36cd7d22bb72f81109 Mon Sep 17 00:00:00 2001 From: 4Z4T4R Date: Thu, 7 Jan 2021 20:32:12 -0800 Subject: [PATCH 08/16] Meteor AutoForm v7.1.0 - Autocomplete - Now, after selecting from the autocomplete options, the field keeps focus so that one may tab to the next field. --- inputTypes/autocomplete/autocomplete.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 67e51980..342a9ed1 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -162,6 +162,7 @@ Template.afAutocomplete.onRendered(function () { $input.val(dataLabel) $hidden.val(dataValue) clearDropdown(e, false) + $input.focus() }) } else if (e.originalEvent.key !== 'Backspace') { @@ -171,6 +172,7 @@ Template.afAutocomplete.onRendered(function () { $input.val(result[0].label) $hidden.val(result[0].value) clearDropdown(e, false) + $input.focus() } else { // no results, hide @@ -208,6 +210,7 @@ Template.afAutocomplete.onRendered(function () { $input.val(enterLabel) $hidden.val(enterValue) clearDropdown(e, false) + $input.focus() } } } From b5ba2e42d7150694b8c2c78f5803a445195278a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Tue, 19 Jan 2021 17:49:25 +0100 Subject: [PATCH 09/16] inputTypes autocomplete update options reactively --- inputTypes/autocomplete/autocomplete.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 342a9ed1..38539042 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -69,6 +69,11 @@ Template.afAutocomplete.onRendered(function () { // get the instance items // defined in several ways const me = Template.instance() + const items = new ReactiveVar([]) + me.autorun(() => { + const data = Template.currentData() + items.set(data.items) + }) // secure the dom so multiple autocompletes don't clash const $input = me.$('input[type="text"]') @@ -127,7 +132,7 @@ Template.afAutocomplete.onRendered(function () { // ensure hidden and visible values match for validation $hidden.val($input.val()) // filter results from visible input value - const result = me.data.items.filter((i) => { + const result = items.get().filter((i) => { const reg = new RegExp(e.target.value, 'gi') return reg.test(i.label) }) From fed965dd53788b6466f7828b0f28940f50510f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Tue, 19 Jan 2021 17:53:28 +0100 Subject: [PATCH 10/16] inputTypes add missing ReactiveVar import --- inputTypes/autocomplete/autocomplete.js | 1 + 1 file changed, 1 insertion(+) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 38539042..3897d71d 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -1,4 +1,5 @@ import { Template } from 'meteor/templating' +import { ReactiveVar } from 'meteor/reactive-var' AutoForm.addInputType('autocomplete', { template: 'afAutocomplete', From a16bf6681b7737b4bcb44845ec1b797e8d45915d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Thu, 21 Jan 2021 23:17:29 +0100 Subject: [PATCH 11/16] inputTypes autocomplete added explicit touch support --- inputTypes/autocomplete/autocomplete.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 3897d71d..3f7a34e4 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -230,4 +230,9 @@ Template.afAutocomplete.onRendered(function () { $input.dblclick((e) => { callback(e) }) + + // show on double click + $input.on('touchstart', (e) => { + callback(e) + }) }) From 2558865403728c7acfa376a55a3e65b7ea0ba902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Fri, 22 Jan 2021 14:03:42 +0100 Subject: [PATCH 12/16] autocomplete ui and reactivity fixed --- inputTypes/autocomplete/autocomplete.js | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 3f7a34e4..d89767d2 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -17,16 +17,17 @@ AutoForm.addInputType('autocomplete', { }, contextAdjust: function (context) { context.atts.autocomplete = 'off' - const itemAtts = { ...context.atts } + const { ...itemAtts } = context.atts // remove non-essential atts from visible input - const visibleAtts = Object.assign({}, { ...context.atts }) - const keys = ['data-schema-key', 'id', 'name'] - keys.forEach(key => { + const visibleAtts = Object.assign({}, context.atts) + + ;['data-schema-key', 'id', 'name'].forEach(key => { delete visibleAtts[key] }) + // add form-control to remaining classes - context.visibleAtts = AutoForm.Utility.addClass({ ...visibleAtts }, 'form-control') - context.atts = AutoForm.Utility.addClass({ ...itemAtts }, 'form-control') + context.visibleAtts = visibleAtts + // build items list context.items = [] @@ -121,6 +122,11 @@ Template.afAutocomplete.onRendered(function () { } }) + const updateValue = value => { + $hidden.val(value) + $hidden.trigger('change') + } + // clear on blur? // TODO: Figure out how blur won't block "click" // $input.blur((e)=>{ clearDropdown(e) }) @@ -131,7 +137,7 @@ Template.afAutocomplete.onRendered(function () { if (/ArrowDown|ArrowUp|ArrowLeft|ArrowRight|Enter|Escape/.test(e.originalEvent.key) === false) { // we're typing // ensure hidden and visible values match for validation - $hidden.val($input.val()) + updateValue($input.val()) // filter results from visible input value const result = items.get().filter((i) => { const reg = new RegExp(e.target.value, 'gi') @@ -166,7 +172,7 @@ Template.afAutocomplete.onRendered(function () { const dataValue = me.$(e.target).attr('data-value') const dataLabel = me.$(e.target).attr('data-label') $input.val(dataLabel) - $hidden.val(dataValue) + updateValue(dataValue) clearDropdown(e, false) $input.focus() }) @@ -176,7 +182,7 @@ Template.afAutocomplete.onRendered(function () { // bc we all make mistakes if (result.length === 1) { $input.val(result[0].label) - $hidden.val(result[0].value) + updateValue(result[0].value) clearDropdown(e, false) $input.focus() } @@ -214,13 +220,17 @@ Template.afAutocomplete.onRendered(function () { const enterValue = $suggestions.children('div').eq(currIndex).attr('data-value') const enterLabel = $suggestions.children('div').eq(currIndex).attr('data-label') $input.val(enterLabel) - $hidden.val(enterValue) + updateValue(enterValue) clearDropdown(e, false) $input.focus() } } } + $input.blur(() => { + $hidden.trigger('blur') // triggers re-validation + }) + // detect keystrokes $input.keyup((e) => { callback(e) @@ -233,6 +243,7 @@ Template.afAutocomplete.onRendered(function () { // show on double click $input.on('touchstart', (e) => { + $hidden.trigger('touchstart') callback(e) }) }) From 68d5d4c352d232cad665ec9c1feb7e6120dad18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Tue, 26 Jan 2021 21:12:45 +0100 Subject: [PATCH 13/16] inputTypes autoComplete added comment on updateValue function --- inputTypes/autocomplete/autocomplete.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index d89767d2..0cfb7c0b 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -122,6 +122,9 @@ Template.afAutocomplete.onRendered(function () { } }) + /** + * Ensure reactivity when changing the hidden value + */ const updateValue = value => { $hidden.val(value) $hidden.trigger('change') From 3077ce1abf80810a8f1c778289ad3760c7871bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Tue, 26 Jan 2021 21:13:25 +0100 Subject: [PATCH 14/16] downgraded version to 7.0.0 until final release to avoid ci conflicts --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 0209b5b3..d0d404a6 100644 --- a/package.js +++ b/package.js @@ -4,7 +4,7 @@ Package.describe({ summary: 'Easily create forms with automatic insert and update, and automatic reactive validation.', git: 'https://github.com/aldeed/meteor-autoform.git', - version: '7.1.0' + version: '7.0.0' }) Package.onUse(function (api) { From 6a0df5e7013bc29baab4855df5697a26cad1f1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Thu, 11 Feb 2021 20:53:53 +0100 Subject: [PATCH 15/16] inputTypes autocomplete corrected event bubbling on click and tab --- inputTypes/autocomplete/autocomplete.js | 59 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index 0cfb7c0b..c8035e51 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -107,7 +107,7 @@ Template.afAutocomplete.onRendered(function () { $input.keydown((e) => { // prevent form submit from "Enter/Return" if showing if ( - /Enter/.test(e.originalEvent.key) === true && + /Enter|Tab/.test(e.originalEvent.key) === true && showing === true ) { e.preventDefault() @@ -130,14 +130,10 @@ Template.afAutocomplete.onRendered(function () { $hidden.trigger('change') } - // clear on blur? - // TODO: Figure out how blur won't block "click" - // $input.blur((e)=>{ clearDropdown(e) }) - const callback = function (e) { // only populate when typing characters or deleting // otherwise, we are navigating - if (/ArrowDown|ArrowUp|ArrowLeft|ArrowRight|Enter|Escape/.test(e.originalEvent.key) === false) { + if (/ArrowDown|ArrowUp|ArrowLeft|ArrowRight|Enter|Escape|Tab/.test(e.originalEvent.key) === false) { // we're typing // ensure hidden and visible values match for validation updateValue($input.val()) @@ -157,7 +153,7 @@ Template.afAutocomplete.onRendered(function () { currIndex = -1 for (let i = 0; i < len; i++) { // populate suggestions - html = `` + html = `` $suggestions.append(html) } $suggestions.addClass('show') @@ -167,11 +163,22 @@ Template.afAutocomplete.onRendered(function () { // clear any manual navigated selections on hover $suggestions.children().hover((e) => { $suggestions.children().removeClass('active') - currIndex = -1 + const $target = me.$(e.target) + $target.addClass('active') + currIndex = Number.parseInt($target.data('index'), 10) + + // make sure showing remains true + showing = true + }) + + // prevent blur when clicking on a suggestion! + $suggestions.children().on('mousedown', e => { + e.preventDefault() + e.stopPropagation() }) // choose an answer on click - $suggestions.children().click((e) => { + $suggestions.children().on('click', (e) => { const dataValue = me.$(e.target).attr('data-value') const dataLabel = me.$(e.target).attr('data-label') $input.val(dataLabel) @@ -215,7 +222,7 @@ Template.afAutocomplete.onRendered(function () { $suggestions.children().removeClass('active') $suggestions.children('div').eq(--currIndex).addClass('active') } - else if (/Enter/.test(e.originalEvent.key) === true) { + else if (/Enter|Tab/.test(e.originalEvent.key) === true) { // we're selecting if (currIndex === -1) { currIndex = 0 @@ -230,17 +237,28 @@ Template.afAutocomplete.onRendered(function () { } } - $input.blur(() => { + // mousedown triggers before blur, so we can check if mousedown is connected + // to a suggestion element and this prevent further bubbling to blur: + // https://stackoverflow.com/a/12092486 + $input.on('mousedown', e => { + if (me.$(e.currentTarget).data('suggestion')) { + e.preventDefault() + e.stopPropagation() + } + }) + + $input.on('blur', e => { $hidden.trigger('blur') // triggers re-validation + clearDropdown(e, false) }) // detect keystrokes - $input.keyup((e) => { + $input.on('keyup', e => { callback(e) }) // show on double click - $input.dblclick((e) => { + $input.on('dblclick', (e) => { callback(e) }) @@ -250,3 +268,18 @@ Template.afAutocomplete.onRendered(function () { callback(e) }) }) + +Template.afAutocomplete.onDestroyed(function () { + const instance = this + const $input = instance.$('input[type="text"]') + $input.off() + + const $hidden = instance.$('input[type="hidden"]') + $hidden.off() + + const $container = instance.$('.dropdown') + $container.off() + + const $suggestions = instance.$('.dropdown-menu') + $suggestions.off() +}) From 738c91ef180642fe30c1994ce2787e9e64b97adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Wed, 31 Mar 2021 22:25:06 +0200 Subject: [PATCH 16/16] inputTypes autocomplete emit change event only on empty (falsy) or valid (in selectOptons) value --- inputTypes/autocomplete/autocomplete.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/inputTypes/autocomplete/autocomplete.js b/inputTypes/autocomplete/autocomplete.js index c8035e51..3940436d 100644 --- a/inputTypes/autocomplete/autocomplete.js +++ b/inputTypes/autocomplete/autocomplete.js @@ -72,9 +72,12 @@ Template.afAutocomplete.onRendered(function () { // defined in several ways const me = Template.instance() const items = new ReactiveVar([]) + let isOption + me.autorun(() => { const data = Template.currentData() items.set(data.items) + isOption = value => data.selectOptions.find(option => option.value === value) }) // secure the dom so multiple autocompletes don't clash @@ -123,11 +126,15 @@ Template.afAutocomplete.onRendered(function () { }) /** - * Ensure reactivity when changing the hidden value + * Ensure reactivity when changing the hidden value to a valid option or a + * falsy value (= deleting the value / clearing the field) */ const updateValue = value => { $hidden.val(value) - $hidden.trigger('change') + + if (!value || isOption(value)) { + $hidden.trigger('change') + } } const callback = function (e) {