Bootstrap v4: Marquage réactif de plusieurs types de caractères

NOTES :

  1. Cet article est une extension d’un article précédent de Bootstrap v4: comprendre et append des classes personnalisées. Mon objective est d’apprendre à append ma propre classe personnalisée à ma copie locale de Bootstrap pour implémenter des balises. Cette classe serait basée sur la classe d’ alerte , qui dispose d’une partie du travail de base (forme non compressible).

  2. Cet article traite de la mise en œuvre des mécanismes de typeahead à valeurs multiples dans textarea . Ainsi, dans le MWE fourni, les trois composants principaux (forme masquée, zone de saisie de type tête de page, balises) sont affichés côte à côte, les suggestions apparaissant ci-dessus. CSS et JS pour “emstackr” ces éléments les uns sur les autres et s’assurer que le texte / les balises sont en dehors du cadre de la publication. Cependant, je prévois de mettre cela dans un futur post.

  3. Je suis nouveau sur jQuery . J’utilise généralement soit la vanille JS ou d3 . Comme je voulais faire de cette partie de ma propre classe Boostrap (voir question liée), j’ai utilisé autant de jQuery qu’il s’est passé sans lutte excessive. Cela dit, il y a des parties mélangeant d3 et jQuery . Si vous souhaitez me tenir au courant des meilleures pratiques pour effectuer l’équivalent dans jQuery, cela serait apprécié, mais pas obligatoire (par exemple, la liaison de données, en ajoutant des éléments, le cas échéant, sont nécessaires, etc.).

  4. Il existe plusieurs publications sur plusieurs types de valeur, et / ou balisage sur plusieurs lignes, etc. Les réponses les plus courantes à ces publications sont similaires à “non pris en charge”, mais vous pouvez les implémenter vous-même, ou voir tagsinput ou Select2 . Aucune de ces bibliothèques ne supporte ce que je voulais au final, d’où l’approche à faire soi-même. La question est de savoir comment j’ai fait cela.

  5. Pour obtenir un balisage multiligne, nous devons utiliser un “pseudo-formulaire” (un formulaire qui prend les entrées de l’utilisateur et qui, en coulisse, met à jour le formulaire réel). Certaines personnes – y compris moi-même – y sont vivement opposées. Si vous connaissez un meilleur moyen de le faire. Je serais extatique d’apprendre.


Avec cela à l’écart, à la question:

Question

J’aimerais avoir le marquage d’entrée pour une zone de textarea où les balises sont empilées verticalement et où la saisie de l’utilisateur a la fonction typeahead activée (plus certaines fonctionnalités personnalisées, mais ne prenons pas une longueur d’avance sur nous-mêmes).

Ci-dessous, je fournis un MWE de la mécanique nécessaire à sa fabrication (bien que certains styles soient encore manquants – voir la note 2 ). Cependant, le tout semble un peu lent ou déformé … du moins par rapport à Twitter-typeahead à entrée unique. Je suppose donc que, quelque part, j’utilise les mauvais écouteurs d’événement, que je commande mal les écouteurs d’événement ou que la logique que j’utilise est incorrecte.

La logique est à peu près la suivante:

 if keydown in [enter, ","]: // logic of function "doneTyping" text = parse(text) // get text from textarea and grab latest value if text is valid: renderTag(text) // put the text in a tag element updateHiddenForm(text) // update the hidden form to include the tag else: notifyUserOfInvalidTag(text) // alert user else: // logic of function "stillTyping" suggest = bloodhoundSearch(text) // use twitter typeahead updateSuggestionBox(suggest) // display results from typeahead 

La logique ci-dessus permet de planifier l’interface entre les entrées utilisateur, de créer des balises et de mettre à jour le formulaire masqué. La suppression des tags est gérée séparément (et également implémentée dans le MWE). Certaines petites fonctionnalités implémentées dans MWE pourraient poser problème (les touches fléchées modifieront la suggestion sélectionnée et “entrer” ou “,” marquera ensuite cette suggestion).

Après tout cela, je vous serais reconnaissant de bien vouloir comprendre ce que j’ai fait et pourquoi cela fonctionne mal. De plus, j’espère que d’autres pourront tirer profit de ce type d’exemple, qui montre comment manipuler et utiliser personnellement Bloodhound pour leurs propres besoins d’événements / types personnalisés.

Exemple de travail minimal

(fonctionne mieux en plein écran)

 // mta = my-textarea var mta = '#my-textarea' var mtaTH = mta + '-typeahead' var mtaTHL = mta + '-typeahead-list' var mtaTHLC = mta + '-typeahead-list-container' var mtaTHT = mta + "-typeahead-text" var mtaTHTP = mta + "-typeahead-text-parsed" var mtaTL = mta + '-tagged-list' var mtaTLC = mta + '-tagged-list-container' var validatedTagsInHiddenForm = [] var maxSuggestions = 5 var confirmInputKeys = [13, 188] //enter, comma var debug = false $(document).ready(function() { dogs = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace, // `genes` is an array of gene names local: ["Affenpinscher", "Afghan Hound", "Airedale Terrier", "Akita", "Alaskan Malamute", "American English Coonhound", "American Eskimo Dog (Miniature)", "American Eskimo Dog (Standard)", "American Eskimo Dog (Toy)", "American Foxhound", "American Hairless Terrier", "American Staffordshire Terrier", "American Water Spaniel", "Anatolian Shepherd Dog", "Australian Cattle Dog", "Australian Shepherd", "Australian Terrier", "Azawakh", "Basenji", "Basset Hound", "Beagle", "Bearded Collie", "Beauceron", "Bedlington Terrier", "Belgian Malinois", "Belgian Sheepdog", "Belgian Tervuren", "Bergamasco", "Berger Picard", "Bernese Mountain Dog", "Bichon Frise", "Black and Tan Coonhound", "Black Russian Terrier", "Bloodhound", "Bluetick Coonhound", "Boerboel", "Border Collie", "Border Terrier", "Borzoi", "Boston Terrier", "Bouvier des Flandres", "Boxer", "Boykin Spaniel", "Briard", "Brittany", "Brussels Griffon", "Bull Terrier", "Bulldog", "Bullmastiff", "Cairn Terrier", "Canaan Dog", "Cane Corso", "Cardigan Welsh Corgi", "Cavalier King Charles Spaniel", "Cesky Terrier", "Chesapeake Bay Resortingever", "Chihuahua", "Chinese Crestd Dog", "Chinese Shar Pei", "Chinook", "Chow Chow", "Cirneco dell'Etna", "Clumber Spaniel", "Cocker Spaniel", "Collie", "Coton de Tulear", "Curly-Coated Resortingever", "Dachshunds", "Dalmatian", "Dandie Dinmont Terrier", "Doberman Pinscher", "Dogue de Bordeaux", "English Cocker Spaniel", "English Foxhound", "English Setter", "English Springer Spaniel", "English Toy Spaniel", "Entlebucher Mountain Dog", "Field Spaniel", "Finnish Lapphund", "Finnish Spitz", "Flat-Coated Resortingever", "French Bulldog", "German Pinscher", "German Shepherd Dog", "German Shorthaired Pointer", "German Wirehaired Pointer", "Giant Schnauzer", "Glen of Imaal Terrier", "Golden Resortingever", "Gordon Setter", "Great Dane", "Great Pyrenees", "Greater Swiss Mountain Dog", "Greyhound", "Harrier", "Havanese", "Ibizan Hound", "Icelandic Sheepdog", "Irish Red and White Setter", "Irish Setter", "Irish Terrier", "Irish Water Spaniel", "Irish Wolfhound", "Italian Greyhound", "Japanese Chin", "Keeshond", "Kerry Blue Terrier", "Komondor", "Kuvasz", "Labrador Resortingever", "Lagotto Romagnolo", "Lakeland Terrier", "Leonberger", "Lhasa Apso", "Löwchen", "Maltese", "Manchester Terrier", "Mastiff", "Miniature American Shepherd", "Miniature Bull Terrier", "Miniature Pinscher", "Miniature Schnauzer", "Neapolitan Mastiff", "Newfoundland", "Norfolk Terrier", "Norwegian Buhund", "Norwegian Elkhound", "Norwegian Lundehund", "Norwich Terrier", "Nova Scotia Duck-Tolling Resortingever", "Old English Sheepdog", "Otterhound", "Papillon", "Parson Russell Terrier", "Pekingese", "Pembroke Welsh Corgi", "Petit Basset Griffon Vendéen", "Pharaoh Hound", "Plott", "Pointer", "Polish Lowland Sheepdog", "Pomeranian", "Poodle", "Portuguese Podengo Pequeno", "Portuguese Water Dog", "Pug", "Puli", "Pumi", "Pyrenean Shepherd", "Rat Terrier", "Redbone Coonhound", "Rhodesian Ridgeback", "Rottweiler", "Russell Terrier", "St. Bernard", "Saluki", "Samoyed", "Schipperke", "Scottish Deerhound", "Scottish Terrier", "Sealyham Terrier", "Shetland Sheepdog", "Shiba Inu", "Shih Tzu", "Siberian Husky", "Silky Terrier", "Skye Terrier", "Sloughi", "Smooth Fox Terrier", "Soft-Coated Wheaten Terrier", "Spanish Water Dog", "Spinone Italiano", "Staffordshire Bull Terrier", "Standard Schnauzer", "Sussex Spaniel", "Swedish Vallhund", "Tibetan Mastiff", "Tibetan Spaniel", "Tibetan Terrier", "Toy Fox Terrier", "Treeing Walker Coonhound", "Vizsla", "Weimaraner", "Welsh Springer Spaniel", "Welsh Terrier", "West Highland White Terrier", "Whippet", "Wire Fox Terrier", "Wirehaired Pointing Griffon", "Wirehaired Vizsla", "Xoloitzcuintli", "Yorkshire Terrier"], templates: { empty: [ '
Unable to find an entry of that name
' ] } }); $(mtaTH).on('keyup', function(event) { var txt = $(mtaTH).val() $(mtaTHT).html(txt) // helps debug parser if (confirmInputKeys.includes(event.which)) { doneTyping(txt) } else { stillTyping(txt) } }) $(mtaTH).on('keydown', function(event) { if ([37, 38].includes(event.which)) { // left, up arrow event.stopPropagation() changeSelectedSuggestion('up') } else if ([39, 40].includes(event.which)) { // right down arrow event.stopPropagation() changeSelectedSuggestion('down') } else { return } }) }) function changeSelectedSuggestion(direction) { var active = d3.select(mtaTHL).select('li.active') if (active.empty()) { return } if (direction == 'up') { var sib = d3.select(active.node().previousElementSibling) if (sib.empty()) { sib = d3.select(mtaTHL).select('li:last-child') } active.classed('active', false) sib.classed('active', true) } if (direction == 'down') { var sib = d3.select(active.node().nextSibling) if (sib.empty()) { sib = d3.select(mtaTHL).select(':first-child') } active.classed('active', false) sib.classed('active', true) } } function doneTyping(txt) { var txt = parse(txt) $(mtaTHTP).html(txt) // helps debug parser if (debug) { console.log('done typeing with ' + txt) } var suggestedItems = d3.select(mtaTHL).select('li.active') if (suggestedItems.empty()) { return } var suggestedElement = suggestedItems.datum() // first item in the list, use data for the list itself var tags = tagIt(suggestedElement) if (tags) { syncTagsAndHiddenForm(tags) syncTagsAndTypeahead(tags) } d3.select(mtaTHL).selectAll('li').remove() $(mta).scrollTop(d3.select(mta).attr("scrollHeight")) $(mtaTH).scrollTop(d3.select(mtaTH).attr("scrollHeight")) } function tagIt(txt) { var alreadyTaggedItems = d3.select(mtaTL).selectAll('li') var tags = [] // if already tags, include them in this list if (!alreadyTaggedItems.empty()) { tags = tags.concat(alreadyTaggedItems.data()) } // if already in list if (tags.includes(txt)) { return } tags.push(txt) // bind new data if (alreadyTaggedItems.size() < tags.length) { var lis = alreadyTaggedItems.data(tags).enter() .append('li').attr('id', function(d, i) { return 'tag-' + d }) .classed('tag', true) .classed('alert', true) .classed('alert-primary', true) .classed('alert-dismissible', true) .classed('fade', true) .classed('show', true) lis.append('span').html(function(d, i) { return d }) lis.append('button').attr('type', 'button').html("×") .on('click', function(d, i) { removeTag(d, i) }) .classed('close', true) } return tags } function removeTag(d, i) { var tags = $(mta).val().split('\n') tags.splice(tags.indexOf(d), 1) // remove tag in place d3.select('[id="tag-' + d + '"').remove() if (debug) { console.log('tags kept after removal of ' + d, tags) } syncTagsAndHiddenForm(tags) syncTagsAndTypeahead(tags) } function syncTagsAndTypeahead(tags) { // $(mtaTH).val(tags.join(',')) var s = "" tags.map(function(tag, ind) { s += tag + "\n" }) $(mtaTH).val(s) } function syncTagsAndHiddenForm(tags) { // $(mta).val(tags.join(',')) var s = "" tags.map(function(tag, ind) { s += tag + '\n' }) $(mta).val(s) } function stillTyping(txt) { var txt = parse(txt) if (debug) { console.log('Still Typing. Looking for suggestions based on ' + txt) } dogs.search(txt, syncSuggestions) } function syncSuggestions(suggestions) { if (debug) { console.log(suggestions) } var filtered = suggestions.slice(0, maxSuggestions) var lis = d3.select(mtaTHL).selectAll('li') if (lis.size() filtered.length) { // too many, remove excess lis.data(filtered).exit().remove() } lis = d3.select(mtaTHL).selectAll('li') lis.data(filtered) .html(function(d, i) { return d }) // update suggestions .classed('list-group-item', true) var anyActive = false lis.each(function() { if (d3.select(this).classed('active')) { anyActive = true } }) if (!anyActive) { lis.classed('active', function(d, i) { if (i == 0) { return true } return false }) } } function parse(txt) { return lastOfSplitBy(lastOfSplitBy(txt, '\n'), ',') } function lastOfSplitBy(txt, by) { // if (txt[txt.length-1] == by) { txt = txt.slice(0, txt.length-1) } var t = txt.split(by) return t[t.length - 1] }
 code kw { /* key-work style */ color: blue; font-weight: bolder; } code bool { /* boolean style */ color: orange; font-weight: bolder; text-transform: uppercase; } code cm { /* comment style */ color: gray; } code cm::before { content: "// " } li.tag { list-style: none; } 
       

Suggestions

Suggestions based on input text:

Parser is using:

id="my-textarea-typeahead-list"

The "hidden" form

id="my-textarea"

Typeahead

id="my-textarea-typeahead"

Tags

id="my-textarea-tagged-list"

Logic

 if keydown in [enter, ,]:  logic of function "doneTyping"  text = parse(text) get text from textarea and grab latest value if text is valid: renderTag(text)put the text in a tag element updateHiddenForm(text)update the hidden form to include the tag else: notifyUserOfInvalidTag(text)alert user else: logic of function "stillTyping"  suggest = bloodhoundSearch(text)use twitter typeahead updateSuggestionBox(suggest)display results from typeahead