3
0
Fork 0

feat: add CopyableTextWidget and <wutta-copyable-text> component

This commit is contained in:
Lance Edgar 2025-12-23 22:57:16 -06:00
parent 49c001c9ad
commit 70950ae9b8
3 changed files with 117 additions and 0 deletions

View file

@ -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.

View file

@ -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"

View file

@ -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):