From 2b6d88105cc77d8d0f2db4323471ad7f555f8cbb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 8 Jun 2019 13:46:00 -0500 Subject: [PATCH] Add support for Buefy autocomplete; several other form tweaks at least the Edit User form should work now, for instance --- tailbone/forms/core.py | 23 +++++ .../static/js/tailbone.buefy.autocomplete.js | 91 +++++++++++++++++++ .../static/js/tailbone.buefy.oncebutton.js | 3 + tailbone/static/themes/falafel/css/forms.css | 9 ++ tailbone/templates/autocomplete.mako | 31 ++++++- .../templates/deform/autocomplete_jquery.pt | 21 ++++- tailbone/templates/deform/checkbox.pt | 27 ++++-- tailbone/templates/deform/checked_password.pt | 60 ++++++++++++ tailbone/templates/deform/select.pt | 8 +- tailbone/templates/deform/textarea.pt | 27 ++++++ tailbone/templates/deform/textinput.pt | 32 +++++++ tailbone/templates/form.mako | 42 ++++++++- tailbone/templates/forms/deform_buefy.mako | 13 +-- tailbone/templates/master/delete.mako | 26 ++++-- tailbone/templates/master/index.mako | 13 ++- tailbone/templates/themes/falafel/base.mako | 6 ++ 16 files changed, 390 insertions(+), 42 deletions(-) create mode 100644 tailbone/static/js/tailbone.buefy.autocomplete.js create mode 100644 tailbone/templates/deform/checked_password.pt create mode 100644 tailbone/templates/deform/textarea.pt create mode 100644 tailbone/templates/deform/textinput.pt diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 84169a06..4f94163c 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -760,6 +760,29 @@ class Form(object): context['render_field_readonly'] = self.render_field_readonly return render(template, context) + def get_vuejs_model_value(self, field): + """ + This method must return "raw" JS which will be assigned as the initial + model value for the given field. This JS will be written as part of + the overall response, to be interpreted on the client side. + """ + if isinstance(field.schema.typ, colander.Date): + # TODO: don't recall why "always null" here? + return 'null' + + if isinstance(field.schema.typ, deform.FileData): + # TODO: don't recall why "always null" here? + return 'null' + + if isinstance(field.schema.typ, colander.Set): + if field.cstruct is colander.null: + return '[]' + + if field.cstruct is colander.null: + return 'null' + + return json.dumps(field.cstruct) + def messages_json(self, messages): dump = json.dumps(messages) dump = dump.replace("'", ''') diff --git a/tailbone/static/js/tailbone.buefy.autocomplete.js b/tailbone/static/js/tailbone.buefy.autocomplete.js new file mode 100644 index 00000000..669b3c1f --- /dev/null +++ b/tailbone/static/js/tailbone.buefy.autocomplete.js @@ -0,0 +1,91 @@ + +const TailboneAutocomplete = { + + template: '#tailbone-autocomplete-template', + + props: { + name: String, + serviceUrl: String, + value: String, + initialLabel: String, + }, + + data() { + let selected = null + if (this.value) { + selected = { + value: this.value, + label: this.initialLabel, + } + } + return { + data: [], + selected: selected, + isFetching: false, + autocompleteValue: this.value, + } + }, + + methods: { + + clearSelection() { + this.selected = null + this.autocompleteValue = null + this.$nextTick(function() { + this.$refs.autocomplete.focus() + }) + + // TODO: should emit event for caller logic (can they cancel?) + // $('#' + oid + '-textbox').trigger('autocompletevaluecleared'); + }, + + // TODO: should we allow custom callback? or is event enough? + // function (oid) { + // $('#' + oid + '-textbox').on('autocompletevaluecleared', function() { + // ${cleared_callback}(); + // }); + // } + + selectionMade(option) { + this.selected = option + + // TODO: should emit event for caller logic (can they cancel?) + // $('#' + oid + '-textbox').trigger('autocompletevalueselected', + // [ui.item.value, ui.item.label]); + }, + + // TODO: should we allow custom callback? or is event enough? + // function (oid) { + // $('#' + oid + '-textbox').on('autocompletevalueselected', function(event, uuid, label) { + // ${selected_callback}(uuid, label); + // }); + // } + + itemSelected(value) { + this.$emit('input', value) + }, + + // TODO: buefy example uses `debounce()` here and perhaps we should too? + // https://buefy.org/documentation/autocomplete + getAsyncData: function (entry) { + if (entry.length < 3) { + this.data = [] + return + } + this.isFetching = true + this.$http.get(this.serviceUrl + '?term=' + encodeURIComponent(entry)) + .then(({ data }) => { + this.data = data + }) + .catch((error) => { + this.data = [] + throw error + }) + .finally(() => { + this.isFetching = false + }) + }, + }, +} + +Vue.component('tailbone-autocomplete', TailboneAutocomplete) diff --git a/tailbone/static/js/tailbone.buefy.oncebutton.js b/tailbone/static/js/tailbone.buefy.oncebutton.js index 3af53276..c8e7e1e1 100644 --- a/tailbone/static/js/tailbone.buefy.oncebutton.js +++ b/tailbone/static/js/tailbone.buefy.oncebutton.js @@ -10,6 +10,8 @@ const OnceButton = { ':title="title"', ':disabled="buttonDisabled"', '@click="clicked"', + 'icon-pack="fas"', + ':icon-left="iconLeft"', '>', '{{ buttonText }}', '' @@ -22,6 +24,7 @@ const OnceButton = { href: String, text: String, title: String, + iconLeft: String, working: String, workingText: String, disabled: Boolean diff --git a/tailbone/static/themes/falafel/css/forms.css b/tailbone/static/themes/falafel/css/forms.css index d23205d2..d816b664 100644 --- a/tailbone/static/themes/falafel/css/forms.css +++ b/tailbone/static/themes/falafel/css/forms.css @@ -13,3 +13,12 @@ white-space: nowrap; width: 18em; } + +.field.is-horizontal .field-body { + min-width: 30em; +} + +.field.is-horizontal .field-body .select, +.field.is-horizontal .field-body .select select { + width: 100%; +} diff --git a/tailbone/templates/autocomplete.mako b/tailbone/templates/autocomplete.mako index 249f8f2e..7ec61f4c 100644 --- a/tailbone/templates/autocomplete.mako +++ b/tailbone/templates/autocomplete.mako @@ -1,4 +1,5 @@ -## -*- coding: utf-8 -*- +## -*- coding: utf-8; -*- + ## TODO: This function signature is getting out of hand... <%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', select=None, selected=None, cleared=None, change_clicked=None, options={})">
@@ -56,3 +57,31 @@ }); + +<%def name="tailbone_autocomplete_template()"> + + diff --git a/tailbone/templates/deform/autocomplete_jquery.pt b/tailbone/templates/deform/autocomplete_jquery.pt index 7231fae3..1533cc2b 100644 --- a/tailbone/templates/deform/autocomplete_jquery.pt +++ b/tailbone/templates/deform/autocomplete_jquery.pt @@ -2,10 +2,14 @@ css_class css_class|field.widget.css_class; oid oid|field.oid; field_display field_display; - style style|field.widget.style" - id="${oid}-container" - class="autocomplete-container"> + style style|field.widget.style; + url url|field.widget.service_url; + use_buefy use_buefy|0;" + tal:omit-tag=""> +
+
+ +
+ + +
diff --git a/tailbone/templates/deform/checkbox.pt b/tailbone/templates/deform/checkbox.pt index d149f7d1..b00ced03 100644 --- a/tailbone/templates/deform/checkbox.pt +++ b/tailbone/templates/deform/checkbox.pt @@ -1,13 +1,26 @@ -
- + +
+ +
+ +
+ + {{ ${vmodel} }} + +
diff --git a/tailbone/templates/deform/checked_password.pt b/tailbone/templates/deform/checked_password.pt new file mode 100644 index 00000000..43657045 --- /dev/null +++ b/tailbone/templates/deform/checked_password.pt @@ -0,0 +1,60 @@ +
+ +
+ ${field.start_mapping()} +
+ +
+
+ +
+ ${field.end_mapping()} +
+ +
+ ${field.start_mapping()} + + + + + ${field.end_mapping()} +
+ +
diff --git a/tailbone/templates/deform/select.pt b/tailbone/templates/deform/select.pt index 3a40226a..8f8ae171 100644 --- a/tailbone/templates/deform/select.pt +++ b/tailbone/templates/deform/select.pt @@ -58,12 +58,14 @@
+ > +
diff --git a/tailbone/templates/deform/textarea.pt b/tailbone/templates/deform/textarea.pt new file mode 100644 index 00000000..25583b4e --- /dev/null +++ b/tailbone/templates/deform/textarea.pt @@ -0,0 +1,27 @@ +
+ +
+ +
+ +
+ + +
+
diff --git a/tailbone/templates/deform/textinput.pt b/tailbone/templates/deform/textinput.pt new file mode 100644 index 00000000..48d4c360 --- /dev/null +++ b/tailbone/templates/deform/textinput.pt @@ -0,0 +1,32 @@ + +
+ + +
+ +
+ + +
+
diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index 2fb050ac..7a323a62 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -11,7 +11,38 @@ ${form.render(buttons=capture(self.render_form_buttons))|n} +<%def name="render_buefy_form()"> +
+ +
+ + <%def name="render_form_complete()"> + % if use_buefy: + ${self.render_form()} + +
+ +
+ % else:
@@ -29,6 +60,7 @@
+ % endif <%def name="modify_tailbone_form()"> @@ -43,8 +75,14 @@ Vue.component('tailbone-form', TailboneForm) + const FormPage = { + template: '#form-page-template' + } + + Vue.component('form-page', FormPage) + new Vue({ - el: '#tailbone-form-app' + el: '#form-page-app' }) @@ -53,6 +91,6 @@ ${self.render_form_complete()} -% if form.use_buefy: +% if use_buefy: ${self.make_tailbone_form_app()} % endif diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index 5ca8b1ac..f49b5dc9 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -91,21 +91,10 @@ % for field in form.fields: % if field in dform: <% field = dform[field] %> - % if isinstance(field.schema.typ, colander.Date): - field_model_${field.name}: null, - % elif isinstance(field.schema.typ, deform.FileData): - field_model_${field.name}: null, - % else: - field_model_${field.name}: ${'null' if field.cstruct is colander.null else json.dumps(field.cstruct)|n}, - % endif + field_model_${field.name}: ${form.get_vuejs_model_value(field)|n}, % endif % endfor % endif } - - -
- -
diff --git a/tailbone/templates/master/delete.mako b/tailbone/templates/master/delete.mako index e6e4fcc2..697eeb47 100644 --- a/tailbone/templates/master/delete.mako +++ b/tailbone/templates/master/delete.mako @@ -20,16 +20,28 @@ % endif -<%def name="render_form()"> +<%def name="render_buefy_form()">
+ % if use_buefy: + + You are about to delete the following ${model_title} and all associated data: + + % else:

You are about to delete the following ${model_title} and all associated data:

+ % endif - ${parent.render_form()} + ${parent.render_buefy_form()} <%def name="render_form_buttons()">
+ % if use_buefy: + + Are you sure about this? + + % else:

Are you sure about this?

+ % endif
${h.form(request.current_route_url(), class_=None if form.use_buefy else 'autodisable')} @@ -39,15 +51,13 @@ - % else: - Whoops, nevermind... - % endif - % if form.use_buefy: - % else: - ${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')} + Whoops, nevermind... + ${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')} % endif ${h.end_form()} diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 4fd3b50a..e161fe3c 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -141,13 +141,12 @@ - - Merge 2 ${model_title_plural} - + + % else: ${h.hidden('uuids')} diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 0cac1e11..8058a9bc 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -1,6 +1,7 @@ ## -*- coding: utf-8; -*- <%namespace file="/grids/nav.mako" import="grid_index_nav" /> <%namespace file="/feedback_dialog_buefy.mako" import="feedback_dialog" /> +<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace name="base_meta" file="/base_meta.mako" /> @@ -30,6 +31,11 @@ + + ## TODO: should move template to JS, then can postpone the JS + ${tailbone_autocomplete_template()} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))} +