feat: add CopyableTextWidget and <wutta-copyable-text> component
This commit is contained in:
parent
49c001c9ad
commit
70950ae9b8
3 changed files with 117 additions and 0 deletions
|
|
@ -147,6 +147,24 @@ class NotesWidget(TextAreaWidget):
|
||||||
readonly_template = "readonly/notes"
|
readonly_template = "readonly/notes"
|
||||||
|
|
||||||
|
|
||||||
|
class CopyableTextWidget(Widget): # pylint: disable=abstract-method
|
||||||
|
"""
|
||||||
|
A readonly text widget which adds a "copy" icon/link just after
|
||||||
|
the text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
if not cstruct:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
return HTML.tag("wutta-copyable-text", **{"text": cstruct})
|
||||||
|
|
||||||
|
def deserialize(self, field, pstruct): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
Custom widget for :class:`python:set` fields.
|
Custom widget for :class:`python:set` fields.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
${self.make_wutta_autocomplete_component()}
|
${self.make_wutta_autocomplete_component()}
|
||||||
${self.make_wutta_button_component()}
|
${self.make_wutta_button_component()}
|
||||||
${self.make_wutta_checked_password_component()}
|
${self.make_wutta_checked_password_component()}
|
||||||
|
${self.make_wutta_copyable_text_component()}
|
||||||
${self.make_wutta_datepicker_component()}
|
${self.make_wutta_datepicker_component()}
|
||||||
${self.make_wutta_timepicker_component()}
|
${self.make_wutta_timepicker_component()}
|
||||||
${self.make_wutta_filter_component()}
|
${self.make_wutta_filter_component()}
|
||||||
|
|
@ -349,6 +350,72 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_wutta_copyable_text_component()">
|
||||||
|
<script type="text/x-template" id="wutta-copyable-text-template">
|
||||||
|
<span>
|
||||||
|
|
||||||
|
<span v-if="!iconFirst">{{ text }}</span>
|
||||||
|
|
||||||
|
<b-tooltip label="Copied!" :triggers="['click']">
|
||||||
|
<a v-if="text"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="copyText()">
|
||||||
|
<b-icon icon="copy" pack="fas" />
|
||||||
|
</a>
|
||||||
|
</b-tooltip>
|
||||||
|
|
||||||
|
<span v-if="iconFirst">{{ text }}</span>
|
||||||
|
|
||||||
|
## dummy input field needed to copy text on *insecure* sites
|
||||||
|
<b-input v-model="legacyText" ref="legacyText" v-show="legacyText" />
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
const WuttaCopyableText = {
|
||||||
|
template: '#wutta-copyable-text-template',
|
||||||
|
props: {
|
||||||
|
text: {required: true},
|
||||||
|
iconFirst: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
## dummy input value needed to copy text on *insecure* sites
|
||||||
|
legacyText: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
async copyText() {
|
||||||
|
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
// this is the way forward, but requires HTTPS
|
||||||
|
navigator.clipboard.writeText(this.text)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// use deprecated 'copy' command, but this just
|
||||||
|
// tells the browser to copy currently-selected
|
||||||
|
// text..which means we first must "add" some text
|
||||||
|
// to screen, and auto-select that, before copying
|
||||||
|
// to clipboard
|
||||||
|
this.legacyText = this.text
|
||||||
|
this.$nextTick(() => {
|
||||||
|
let input = this.$refs.legacyText.$el.firstChild
|
||||||
|
input.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
// re-hide the dummy input
|
||||||
|
this.legacyText = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Vue.component('wutta-copyable-text', WuttaCopyableText)
|
||||||
|
<% request.register_component('wutta-copyable-text', 'WuttaCopyableText') %>
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="make_wutta_datepicker_component()">
|
<%def name="make_wutta_datepicker_component()">
|
||||||
<script type="text/x-template" id="wutta-datepicker-template">
|
<script type="text/x-template" id="wutta-datepicker-template">
|
||||||
<b-datepicker :name="name"
|
<b-datepicker :name="name"
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,38 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.assertNotIn("url", values)
|
self.assertNotIn("url", values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCopyableTextWidget(WebTestCase):
|
||||||
|
|
||||||
|
def make_field(self, node, **kwargs):
|
||||||
|
# TODO: not sure why default renderer is in use even though
|
||||||
|
# pyramid_deform was included in setup? but this works..
|
||||||
|
kwargs.setdefault("renderer", deform.Form.default_renderer)
|
||||||
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
|
def make_widget(self, **kwargs):
|
||||||
|
return mod.CopyableTextWidget(**kwargs)
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
node = colander.SchemaNode(colander.String())
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = self.make_widget()
|
||||||
|
|
||||||
|
self.assertIs(widget.serialize(field, colander.null), colander.null)
|
||||||
|
self.assertIs(widget.serialize(field, None), colander.null)
|
||||||
|
self.assertIs(widget.serialize(field, ""), colander.null)
|
||||||
|
|
||||||
|
result = widget.serialize(field, "hello world")
|
||||||
|
self.assertEqual(
|
||||||
|
result, '<wutta-copyable-text text="hello world"></wutta-copyable-text>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_deserialize(self):
|
||||||
|
node = colander.SchemaNode(colander.String())
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = self.make_widget()
|
||||||
|
self.assertRaises(NotImplementedError, widget.deserialize, field, "hello world")
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaDateWidget(WebTestCase):
|
class TestWuttaDateWidget(WebTestCase):
|
||||||
|
|
||||||
def make_field(self, node, **kwargs):
|
def make_field(self, node, **kwargs):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue