From 3bd7f7e71a1465296b70b35bee4c9d1b8b6c9fa9 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 28 Jun 2016 15:21:41 +0200 Subject: [PATCH] #403 simple implementation of ` --- gradleResources/build.gradle | 1 + gradleResources/js/bsf.js | 22 ++++ .../META-INF/resources/bsf/js/bsf.js | 26 ++++- .../component/inputText/InputText.java | 19 ++++ .../component/inputText/InputTextCore.java | 102 ++++++++++++++++++ .../inputText/InputTextRenderer.java | 101 +++++++++++++++-- .../meta/META-INF/bootsfaces-b.taglib.xml | 66 ++++++++++++ xtext/BootsFaces.jsfdsl | 6 ++ 8 files changed, 335 insertions(+), 8 deletions(-) diff --git a/gradleResources/build.gradle b/gradleResources/build.gradle index afa18c184..70eaa2244 100644 --- a/gradleResources/build.gradle +++ b/gradleResources/build.gradle @@ -155,6 +155,7 @@ def copySpecCssResources = copySpec { from 'css/jquery.minicolors.css' from 'css/sticky-footer-navbar.css' from 'css/bootstrap-datetimepicker.min.css' + from 'css/typeahead.css' } task copyCssResources << { allThemes.each { dest -> diff --git a/gradleResources/js/bsf.js b/gradleResources/js/bsf.js index 61834a3e1..b7587c92c 100644 --- a/gradleResources/js/bsf.js +++ b/gradleResources/js/bsf.js @@ -169,3 +169,25 @@ function treeDataMapper(data) { } return ""; } + +var substringMatcher = function(strs) { + return function findMatches(q, cb) { + var matches, substringRegex; + + // an array that will be populated with substring matches + matches = []; + + // regex used to determine if a string contains the substring `q` + substrRegex = new RegExp(q, 'i'); + + // iterate through the pool of strings and for any string that + // contains the substring `q`, add it to the `matches` array + $.each(strs, function(i, str) { + if (substrRegex.test(str)) { + matches.push(str); + } + }); + + cb(matches); + }; + }; diff --git a/mavenResources/META-INF/resources/bsf/js/bsf.js b/mavenResources/META-INF/resources/bsf/js/bsf.js index a5832b902..f5ce95599 100644 --- a/mavenResources/META-INF/resources/bsf/js/bsf.js +++ b/mavenResources/META-INF/resources/bsf/js/bsf.js @@ -1,5 +1,5 @@ /* - + Copyright 2014 Riccardo Massera (TheCoder4.Eu) BootsFaces JS author: TheonSuccessCallbackCoder4.eu @@ -10,4 +10,26 @@ BsF.ajax.onevent=function(a){if("complete"===a.status){var b=a.source.id.replace BsF.ajax.callAjax=function(a,b,c,d,f,g,h,m){var k=a.id,l=k.replace(/[^a-zA-Z0-9]+/g,"_"),e={};m&&(e.params="BsFEvent="+m);(c=BsF.ajax.resolveJQuery(c))&&null!=c&&(e.render=c);(d=BsF.ajax.resolveJQuery(d))&&null!=d&&(e.execute=d);e[k]=k;BsF.onCompleteCallback[l]=f&&null!=f?f:null;BsF.onErrorCallback[l]=g&&null!=g?g:null;BsF.onSuccessCallback[l]=h&&null!=h?h:null;e.onevent=BsF.ajax.onevent;jsf.ajax.request(a,b,e);$.blockUI&&null!=$.blockUI&&$.blockUI();return!1}; BsF.ajax.resolveJQuery=function(a){if("undefined"==typeof a||null==a)return"";var b="";a=a.split(" ");for(i=0;i[^<]+<\/span>/,'');return a=a.replace(/[^<]+<\/span>/,'')}} -function jq(a){return"#"+a.replace(/(:|\.|\[|\]|,)/g,"\\$1")}function treeDataMapper(a){return a&&"undefined"!==a?a.nodeInternalId+"|#*#|"+a.text+"|#*#|"+a.state.checked+"|#*#|"+a.state.disabled+"|#*#|"+a.state.expanded+"|#*#|"+a.state.selected:""}; \ No newline at end of file +function jq(a){return"#"+a.replace(/(:|\.|\[|\]|,)/g,"\\$1")}function treeDataMapper(a){return a&&"undefined"!==a?a.nodeInternalId+"|#*#|"+a.text+"|#*#|"+a.state.checked+"|#*#|"+a.state.disabled+"|#*#|"+a.state.expanded+"|#*#|"+a.state.selected:""}; + +var substringMatcher = function(strs) { + return function findMatches(q, cb) { + var matches, substringRegex; + + // an array that will be populated with substring matches + matches = []; + + // regex used to determine if a string contains the substring `q` + substrRegex = new RegExp(q, 'i'); + + // iterate through the pool of strings and for any string that + // contains the substring `q`, add it to the `matches` array + $.each(strs, function(i, str) { + if (substrRegex.test(str)) { + matches.push(str); + } + }); + + cb(matches); + }; + }; diff --git a/src/main/java/net/bootsfaces/component/inputText/InputText.java b/src/main/java/net/bootsfaces/component/inputText/InputText.java index 070fa31e8..67c4b50c3 100644 --- a/src/main/java/net/bootsfaces/component/inputText/InputText.java +++ b/src/main/java/net/bootsfaces/component/inputText/InputText.java @@ -111,5 +111,24 @@ public void setTags(boolean _tags) { super.setTags(_tags); } + /** + * Activates the type-ahead aka autocomplete function. The list of values has to be defined in typeahead-values.

+ * Usually this method is called internally by the JSF engine. + */ + @Override + public void setTypeahead(boolean _typeahead) { + if (_typeahead) { + AddResourcesListener.addResourceToHeadButAfterJQuery(C.BSF_LIBRARY, "js/typeahead.js"); + AddResourcesListener.addThemedCSSResource("typeahead.css"); + } + super.setTypeahead(_typeahead); + } + + @Override + public void setTypeaheadValues(String _typeaheadValues) { + setTypeahead(true); + super.setTypeaheadValues(_typeaheadValues); + } + } diff --git a/src/main/java/net/bootsfaces/component/inputText/InputTextCore.java b/src/main/java/net/bootsfaces/component/inputText/InputTextCore.java index 3c805731a..700048869 100644 --- a/src/main/java/net/bootsfaces/component/inputText/InputTextCore.java +++ b/src/main/java/net/bootsfaces/component/inputText/InputTextCore.java @@ -66,6 +66,12 @@ protected enum PropertyKeys { tooltipDelayShow, tooltipPosition, type, + typeahead, + typeaheadHighlight, + typeaheadHint, + typeaheadLimit, + typeaheadMinLength, + typeaheadValues, update, visible; String toString; @@ -739,6 +745,102 @@ public void setType(String _type) { getStateHelper().put(PropertyKeys.type, _type); } + /** + * Activates the type-ahead aka autocomplete function. The list of values has to be defined in typeahead-values.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public boolean isTypeahead() { + return (boolean) (Boolean) getStateHelper().eval(PropertyKeys.typeahead, false); + } + + /** + * Activates the type-ahead aka autocomplete function. The list of values has to be defined in typeahead-values.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeahead(boolean _typeahead) { + getStateHelper().put(PropertyKeys.typeahead, _typeahead); + } + + /** + * Highlights the part of the suggestions that has already been entered. Defaults to true.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public boolean isTypeaheadHighlight() { + return (boolean) (Boolean) getStateHelper().eval(PropertyKeys.typeaheadHighlight, true); + } + + /** + * Highlights the part of the suggestions that has already been entered. Defaults to true.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeaheadHighlight(boolean _typeaheadHighlight) { + getStateHelper().put(PropertyKeys.typeaheadHighlight, _typeaheadHighlight); + } + + /** + * If set to false, the typeahead will not show a hint. Defaults to true.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public boolean isTypeaheadHint() { + return (boolean) (Boolean) getStateHelper().eval(PropertyKeys.typeaheadHint, true); + } + + /** + * If set to false, the typeahead will not show a hint. Defaults to true.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeaheadHint(boolean _typeaheadHint) { + getStateHelper().put(PropertyKeys.typeaheadHint, _typeaheadHint); + } + + /** + * Maximum number of suggestions to be shown. Defaults to 5.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public int getTypeaheadLimit() { + return (int) (Integer) getStateHelper().eval(PropertyKeys.typeaheadLimit, 5); + } + + /** + * Maximum number of suggestions to be shown. Defaults to 5.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeaheadLimit(int _typeaheadLimit) { + getStateHelper().put(PropertyKeys.typeaheadLimit, _typeaheadLimit); + } + + /** + * Minimum number of characters to be entered before a suggestion is shown.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public int getTypeaheadMinLength() { + return (int) (Integer) getStateHelper().eval(PropertyKeys.typeaheadMinLength, 1); + } + + /** + * Minimum number of characters to be entered before a suggestion is shown.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeaheadMinLength(int _typeaheadMinLength) { + getStateHelper().put(PropertyKeys.typeaheadMinLength, _typeaheadMinLength); + } + + /** + * Comma-separated list of values that can be used for the typeahead list.

+ * @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. + */ + public String getTypeaheadValues() { + return (String) getStateHelper().eval(PropertyKeys.typeaheadValues); + } + + /** + * Comma-separated list of values that can be used for the typeahead list.

+ * Usually this method is called internally by the JSF engine. + */ + public void setTypeaheadValues(String _typeaheadValues) { + getStateHelper().put(PropertyKeys.typeaheadValues, _typeaheadValues); + } + /** * Component(s) to be updated with ajax.

* @return Returns the value of the attribute, or null, if it hasn't been set by the JSF file. diff --git a/src/main/java/net/bootsfaces/component/inputText/InputTextRenderer.java b/src/main/java/net/bootsfaces/component/inputText/InputTextRenderer.java index 4fbb8dcff..81875a839 100644 --- a/src/main/java/net/bootsfaces/component/inputText/InputTextRenderer.java +++ b/src/main/java/net/bootsfaces/component/inputText/InputTextRenderer.java @@ -213,7 +213,7 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce if ((autocomplete != null) && (autocomplete.equals("off"))) { rw.writeAttribute("autocomplete", "off", null); } - if (inputText.isTags()) { + if (inputText.isTags() && (!inputText.isTypeahead())) { rw.writeAttribute("data-role", "tagsinput", null); } @@ -237,15 +237,101 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce } Tooltip.activateTooltips(context, inputText); - if (inputText.isTags()) { -// String id = component.getClientId(); -// id = id.replace(":", "\\\\:"); // we need to escape the id for jQuery -// rw.startElement("script", component); -// rw.endElement("script"); + if (inputText.isTypeahead()) { + String id = component.getClientId(); + id = id.replace(":", "_"); // we need to escape the id for jQuery + rw.startElement("script", component); + String typeaheadname = id + "_typeahead"; + if (inputText.isTags()) { + String js = "var engine = new Bloodhound({" + // + "name: '" + typeaheadname + "'," + // + "local: " + getTypeaheadObjectArray(inputText) + "," + // + "datumTokenizer: function(d) {" + // + " return Bloodhound.tokenizers.whitespace(d.val);" + // + "}," + // + "queryTokenizer: Bloodhound.tokenizers.whitespace" + // + "});"; + js += "$('." + id + "').tagsinput({" + // + "typeaheadjs: {" + // + " name: 'animals'," + // + " displayKey: 'val'," + // + " valueKey: 'val'," + // + " source: engine.ttAdapter()" + // + "}" + // + "});";// + rw.writeText(js, null); + + } else { + + String options = ""; + options = addOption(options, "hint:" + inputText.isTypeaheadHint()); + options = addOption(options, "highlight:" + inputText.isTypeaheadHighlight()); + options = addOption(options, "minLength:" + inputText.getTypeaheadMinLength()); + String options2 = ""; + options2 = addOption(options2, "limit:" + inputText.getTypeaheadLimit()); + options2 = addOption(options2, "name:'" + typeaheadname + "'"); + options2 = addOption(options2, "source: substringMatcher(" + getTypeaheadValueArray(inputText) + ")"); + + rw.writeText("$('." + id + "').typeahead({" + options + "},{" + options2 + "});", null); + } + rw.endElement("script"); + } + } + + private String addOption(String options, String newOption) { + if (options.length() > 0) { + options += ","; + } + return options + newOption; + } + + private String getTypeaheadValueArray(InputText inputText) { + String s = inputText.getTypeaheadValues(); + if (null == s) + return null; + s = s.trim(); + if (!s.contains("\'")) { + String[] parts = s.split(","); + StringBuilder b = new StringBuilder(s.length() * 2); + for (String p : parts) { + if (b.length() > 0) { + b.append(','); + } + b.append('\''); + b.append(p.trim()); + b.append('\''); + } + s = b.toString(); } + return "[" + s + "]"; } + private String getTypeaheadObjectArray(InputText inputText) { + String s = inputText.getTypeaheadValues(); + if (null == s) + return null; + s = s.trim(); + if (!s.contains("\'")) { + String[] parts = s.split(","); + StringBuilder b = new StringBuilder(s.length() * 2); + for (String p : parts) { + if (b.length() > 0) { + b.append(','); + } + b.append("{val:"); + b.append('\''); + b.append(p.trim()); + b.append('\''); + b.append('}'); + } + s = b.toString(); + + } + return "[" + s + "]"; + } + + private void generateStyleClass(InputText inputText, ResponseWriter rw) throws IOException { StringBuilder sb; String s; @@ -265,6 +351,9 @@ private void generateStyleClass(InputText inputText, ResponseWriter rw) throws I } sb.append(" ").append(getErrorAndRequiredClass(inputText, inputText.getClientId())); + if (inputText.isTypeahead()) { + sb.append(" ").append(inputText.getClientId().replace(":","_")); + } s = sb.toString().trim(); if (s != null && s.length() > 0) { rw.writeAttribute("class", s, "class"); diff --git a/src/main/meta/META-INF/bootsfaces-b.taglib.xml b/src/main/meta/META-INF/bootsfaces-b.taglib.xml index e127d7553..165d56190 100644 --- a/src/main/meta/META-INF/bootsfaces-b.taglib.xml +++ b/src/main/meta/META-INF/bootsfaces-b.taglib.xml @@ -10154,6 +10154,72 @@ false java.lang.String + + + typeahead + false + java.lang.Boolean + + + + typeahead-highlight + false + java.lang.Boolean + + + + typeaheadHighlight + false + java.lang.Boolean + + + + typeahead-hint + false + java.lang.Boolean + + + + typeaheadHint + false + java.lang.Boolean + + + + typeahead-limit + false + java.lang.Integer + + + + typeaheadLimit + false + java.lang.Integer + + + + typeahead-minLength + false + java.lang.Integer + + + + typeaheadMinLength + false + java.lang.Integer + + + + typeahead-values + false + java.lang.String + + + + typeaheadValues + false + java.lang.String + update diff --git a/xtext/BootsFaces.jsfdsl b/xtext/BootsFaces.jsfdsl index 8045f207f..e0b955d38 100644 --- a/xtext/BootsFaces.jsfdsl +++ b/xtext/BootsFaces.jsfdsl @@ -949,6 +949,12 @@ widget inputText tags Boolean "Show the words of the input text as tags (similar to price tags in the supermarket). You can select one or more tags. The list is sent to the backend bean as a comma-separated list." title inherited "Advisory tooltip information." type "Type of the input. The default is text." + typeahead Boolean "Activates the type-ahead aka autocomplete function. The list of values has to be defined in typeahead-values." + typeahead-limit Integer default "5" "Maximum number of suggestions to be shown. Defaults to 5." + typeahead-highlight Boolean default "true" "Highlights the part of the suggestions that has already been entered. Defaults to true." + typeahead-hint Boolean default "true" "If set to false, the typeahead will not show a hint. Defaults to true." + typeahead-minLength Integer default "1" "Minimum number of characters to be entered before a suggestion is shown." + typeahead-values "Comma-separated list of values that can be used for the typeahead list." update "Component(s) to be updated with ajax." validator javax.faces.validator.Validator inherited "A method expression referring to a method validationg the input." validator-message inherited "Message to display when validation fails."