Add support for Buefy autocomplete; several other form tweaks
at least the Edit User form should work now, for instance
This commit is contained in:
parent
d7e19865de
commit
2b6d88105c
|
@ -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("'", ''')
|
||||
|
|
91
tailbone/static/js/tailbone.buefy.autocomplete.js
Normal file
91
tailbone/static/js/tailbone.buefy.autocomplete.js
Normal file
|
@ -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)
|
|
@ -10,6 +10,8 @@ const OnceButton = {
|
|||
':title="title"',
|
||||
':disabled="buttonDisabled"',
|
||||
'@click="clicked"',
|
||||
'icon-pack="fas"',
|
||||
':icon-left="iconLeft"',
|
||||
'>',
|
||||
'{{ buttonText }}',
|
||||
'</b-button>'
|
||||
|
@ -22,6 +24,7 @@ const OnceButton = {
|
|||
href: String,
|
||||
text: String,
|
||||
title: String,
|
||||
iconLeft: String,
|
||||
working: String,
|
||||
workingText: String,
|
||||
disabled: Boolean
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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={})">
|
||||
<div id="${field_name}-container" class="autocomplete-container">
|
||||
|
@ -56,3 +57,31 @@
|
|||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="tailbone_autocomplete_template()">
|
||||
<script type="text/x-template" id="tailbone-autocomplete-template">
|
||||
<div>
|
||||
|
||||
<b-autocomplete ref="autocomplete"
|
||||
:name="name"
|
||||
v-show="!selected"
|
||||
v-model="autocompleteValue"
|
||||
:data="data"
|
||||
@typing="getAsyncData"
|
||||
@select="selectionMade"
|
||||
@input="itemSelected"
|
||||
keep-first>
|
||||
<template slot-scope="props">
|
||||
{{ props.option.label }}
|
||||
</template>
|
||||
</b-autocomplete>
|
||||
|
||||
<b-button v-if="selected"
|
||||
style="width: 100%; justify-content: left;"
|
||||
@click="clearSelection()">
|
||||
{{ selected.label }} (click to change)
|
||||
</b-button>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -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="">
|
||||
|
||||
<div tal:condition="not use_buefy"
|
||||
id="${oid}-container"
|
||||
class="autocomplete-container">
|
||||
<input type="hidden"
|
||||
name="${name}"
|
||||
id="${oid}"
|
||||
|
@ -98,5 +102,16 @@
|
|||
}
|
||||
);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div tal:condition="use_buefy"
|
||||
tal:define="vmodel vmodel|'field_model_' + name;"
|
||||
tal:omit-tag="">
|
||||
<tailbone-autocomplete name="${name}"
|
||||
service-url="${url}"
|
||||
v-model="${vmodel}"
|
||||
initial-label="${field_display}">
|
||||
</tailbone-autocomplete>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
<div class="checkbox">
|
||||
<input tal:define="name name|field.name;
|
||||
true_val true_val|field.widget.true_val;
|
||||
css_class css_class|field.widget.css_class;
|
||||
style style|field.widget.style;
|
||||
oid oid|field.oid"
|
||||
type="checkbox"
|
||||
<div tal:define="name name|field.name;
|
||||
true_val true_val|field.widget.true_val;
|
||||
css_class css_class|field.widget.css_class;
|
||||
style style|field.widget.style;
|
||||
oid oid|field.oid;
|
||||
use_buefy use_buefy|0;"
|
||||
tal:omit-tag="">
|
||||
|
||||
<div tal:condition="not use_buefy" class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="${name}" value="${true_val}"
|
||||
id="${oid}"
|
||||
tal:attributes="checked cstruct == true_val;
|
||||
class css_class;
|
||||
style style;" />
|
||||
</div>
|
||||
|
||||
<div tal:condition="use_buefy"
|
||||
tal:define="vmodel vmodel|'field_model_' + name;">
|
||||
<b-checkbox name="${name}"
|
||||
v-model="${vmodel}"
|
||||
native-value="${true_val}">
|
||||
{{ ${vmodel} }}
|
||||
</b-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
|
60
tailbone/templates/deform/checked_password.pt
Normal file
60
tailbone/templates/deform/checked_password.pt
Normal file
|
@ -0,0 +1,60 @@
|
|||
<div i18n:domain="deform" tal:omit-tag=""
|
||||
tal:define="oid oid|field.oid;
|
||||
name name|field.name;
|
||||
css_class css_class|field.widget.css_class;
|
||||
style style|field.widget.style;
|
||||
use_buefy use_buefy|0;">
|
||||
|
||||
<div tal:condition="not use_buefy" tal:omit-tag="">
|
||||
${field.start_mapping()}
|
||||
<div>
|
||||
<input type="password"
|
||||
name="${name}"
|
||||
value="${field.widget.redisplay and cstruct or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
id="${oid}"
|
||||
i18n:attributes="placeholder"
|
||||
placeholder="Password"/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="password"
|
||||
name="${name}-confirm"
|
||||
value="${field.widget.redisplay and confirm or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||
id="${oid}-confirm"
|
||||
i18n:attributes="placeholder"
|
||||
placeholder="Confirm Password"/>
|
||||
</div>
|
||||
${field.end_mapping()}
|
||||
</div>
|
||||
|
||||
<div tal:condition="use_buefy">
|
||||
${field.start_mapping()}
|
||||
<b-input type="password"
|
||||
name="${name}"
|
||||
value="${field.widget.redisplay and cstruct or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
id="${oid}"
|
||||
i18n:attributes="placeholder"
|
||||
placeholder="Password">
|
||||
</b-input>
|
||||
<b-input type="password"
|
||||
name="${name}-confirm"
|
||||
value="${field.widget.redisplay and confirm or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||
id="${oid}-confirm"
|
||||
i18n:attributes="placeholder"
|
||||
placeholder="Confirm Password">
|
||||
</b-input>
|
||||
${field.end_mapping()}
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -58,12 +58,14 @@
|
|||
|
||||
<div tal:condition="use_buefy"
|
||||
tal:define="vmodel vmodel|'field_model_' + name;"
|
||||
tal:omit-tag="">
|
||||
>
|
||||
<input type="hidden" name="__start__" value="${name}:sequence"
|
||||
tal:condition="multiple" />
|
||||
<b-select tal:attributes="name name;
|
||||
id oid;
|
||||
placeholder '(please choose)';
|
||||
class string: form-control ${css_class or ''};
|
||||
multiple multiple;
|
||||
:multiple str(multiple).lower();
|
||||
size size;
|
||||
style style;
|
||||
v-model vmodel;
|
||||
|
@ -88,6 +90,8 @@
|
|||
</tal:loop>
|
||||
|
||||
</b-select>
|
||||
<input type="hidden" name="__end__" value="${name}:sequence"
|
||||
tal:condition="multiple" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
27
tailbone/templates/deform/textarea.pt
Normal file
27
tailbone/templates/deform/textarea.pt
Normal file
|
@ -0,0 +1,27 @@
|
|||
<div tal:define="rows rows|field.widget.rows;
|
||||
cols cols|field.widget.cols;
|
||||
css_class css_class|field.widget.css_class;
|
||||
oid oid|field.oid;
|
||||
name name|field.name;
|
||||
style style|field.widget.style;
|
||||
use_buefy use_buefy|0;"
|
||||
tal:omit-tag="">
|
||||
|
||||
<div tal:condition="not use_buefy" tal:omit-tag="">
|
||||
<textarea tal:attributes="rows rows;
|
||||
cols cols;
|
||||
class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
id="${oid}"
|
||||
name="${name}">${cstruct}</textarea>
|
||||
</div>
|
||||
|
||||
<div tal:condition="use_buefy"
|
||||
tal:define="vmodel vmodel|'field_model_' + name;">
|
||||
<b-input type="textarea"
|
||||
name="${name}"
|
||||
v-model="${vmodel}">
|
||||
</b-input>
|
||||
</div>
|
||||
</div>
|
32
tailbone/templates/deform/textinput.pt
Normal file
32
tailbone/templates/deform/textinput.pt
Normal file
|
@ -0,0 +1,32 @@
|
|||
<span tal:define="name name|field.name;
|
||||
css_class css_class|field.widget.css_class;
|
||||
oid oid|field.oid;
|
||||
mask mask|field.widget.mask;
|
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder;
|
||||
style style|field.widget.style;
|
||||
use_buefy use_buefy|0;"
|
||||
tal:omit-tag="">
|
||||
<div tal:condition="not use_buefy" tal:omit-tag="">
|
||||
<input type="text" name="${name}" value="${cstruct}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
id="${oid}"/>
|
||||
<script tal:condition="mask" type="text/javascript">
|
||||
deform.addCallback(
|
||||
'${oid}',
|
||||
function (oid) {
|
||||
$("#" + oid).mask("${mask}",
|
||||
{placeholder:"${mask_placeholder}"});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div tal:condition="use_buefy"
|
||||
tal:define="vmodel vmodel|'field_model_' + name;"
|
||||
tal:omit-tag="">
|
||||
<b-input name="${name}"
|
||||
v-model="${vmodel}">
|
||||
</b-input>
|
||||
</div>
|
||||
</span>
|
|
@ -11,7 +11,38 @@
|
|||
${form.render(buttons=capture(self.render_form_buttons))|n}
|
||||
</%def>
|
||||
|
||||
<%def name="render_buefy_form()">
|
||||
<div class="form">
|
||||
<tailbone-form></tailbone-form>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="render_form_complete()">
|
||||
% if use_buefy:
|
||||
${self.render_form()}
|
||||
<script type="text/x-template" id="form-page-template">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
|
||||
<div class="form-wrapper">
|
||||
${self.render_buefy_form()}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: flex-start;">
|
||||
<div class="object-helpers">
|
||||
${self.object_helpers()}
|
||||
</div>
|
||||
|
||||
<ul id="context-menu">
|
||||
${self.context_menu_items()}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
<div id="form-page-app">
|
||||
<form-page></form-page>
|
||||
</div>
|
||||
% else:
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
|
||||
<div class="form-wrapper">
|
||||
|
@ -29,6 +60,7 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%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'
|
||||
})
|
||||
|
||||
</script>
|
||||
|
@ -53,6 +91,6 @@
|
|||
|
||||
${self.render_form_complete()}
|
||||
|
||||
% if form.use_buefy:
|
||||
% if use_buefy:
|
||||
${self.make_tailbone_form_app()}
|
||||
% endif
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div id="tailbone-form-app">
|
||||
<tailbone-form></tailbone-form>
|
||||
</div>
|
||||
|
|
|
@ -20,16 +20,28 @@
|
|||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="render_form()">
|
||||
<%def name="render_buefy_form()">
|
||||
<br />
|
||||
% if use_buefy:
|
||||
<b-notification type="is-danger" :closable="false">
|
||||
You are about to delete the following ${model_title} and all associated data:
|
||||
</b-notification>
|
||||
% else:
|
||||
<p>You are about to delete the following ${model_title} and all associated data:</p>
|
||||
% endif
|
||||
|
||||
${parent.render_form()}
|
||||
${parent.render_buefy_form()}
|
||||
</%def>
|
||||
|
||||
<%def name="render_form_buttons()">
|
||||
<br />
|
||||
% if use_buefy:
|
||||
<b-notification type="is-danger" :closable="false">
|
||||
Are you sure about this?
|
||||
</b-notification>
|
||||
% else:
|
||||
<p>Are you sure about this?</p>
|
||||
% endif
|
||||
<br />
|
||||
|
||||
${h.form(request.current_route_url(), class_=None if form.use_buefy else 'autodisable')}
|
||||
|
@ -39,15 +51,13 @@
|
|||
<once-button tag="a" href="${form.cancel_url}"
|
||||
text="Whoops, nevermind...">
|
||||
</once-button>
|
||||
% else:
|
||||
<a class="button" href="${form.cancel_url}">Whoops, nevermind...</a>
|
||||
% endif
|
||||
% if form.use_buefy:
|
||||
<once-button type="is-primary" native-type="submit"
|
||||
<once-button type="is-primary is-danger"
|
||||
native-type="submit"
|
||||
text="Yes, please DELETE this data forever!">
|
||||
</once-button>
|
||||
% else:
|
||||
${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')}
|
||||
<a class="button" href="${form.cancel_url}">Whoops, nevermind...</a>
|
||||
${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')}
|
||||
% endif
|
||||
</div>
|
||||
${h.end_form()}
|
||||
|
|
|
@ -141,13 +141,12 @@
|
|||
<input type="hidden"
|
||||
name="uuids"
|
||||
:value="checkedRowUUIDs()" />
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="object-ungroup"
|
||||
:disabled="checkedRows.length != 2">
|
||||
Merge 2 ${model_title_plural}
|
||||
</b-button>
|
||||
<once-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-left="object-ungroup"
|
||||
:disabled="checkedRows.length != 2"
|
||||
text="Merge 2 ${model_title_plural}">
|
||||
</once-button>
|
||||
% else:
|
||||
${h.hidden('uuids')}
|
||||
<button type="submit" class="button">Merge 2 ${model_title_plural}</button>
|
||||
|
|
|
@ -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" />
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -30,6 +31,11 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
## 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__))}
|
||||
|
||||
<header>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
|
|
Loading…
Reference in a new issue