add vueuse useClipboard

This commit is contained in:
Jordan Blasenhauer 2024-06-12 15:23:22 +02:00
parent 76ccbf333e
commit b223644ac5
7 changed files with 178 additions and 90 deletions

View file

@ -302,6 +302,50 @@ body {
@apply absolute w-full h-full border-2 border-red-500 z-10 pointer-events-none outline-red-500;
}
.input-clipboard-container {
@apply rounded-full absolute flex w-full h-full;
}
.editor.input-clipboard-container {
@apply top-2 right-2;
}
.pw-input.input-clipboard-container {
@apply top-1 md:top-1.5 right-[2.25rem];
}
.no-pw-input.input-clipboard-container {
@apply top-1 md:top-1.5 right-2;
}
.input-clipboard-svg {
@apply stroke-gray-800 dark:stroke-gray-300 ;
}
.input-clipboard-button {
@apply transition-all h-5.5 w-5.5 md:h-6 md:w-6 absolute flex block top-0 right-0 text-gray-500 hover:text-gray-900 transition duration-300 ease-in-out cursor-pointer focus:outline-none focus:ring-0 rounded-full hover:bg-gray-100 z-10;
}
.copied.input-clipboard-button {
@apply opacity-100;
}
.not-copied.input-clipboard-button {
@apply opacity-[50%] hover:opacity-100;
}
.input-clipboard-copy {
@apply absolute bg-green-500 text-white px-1 py-0.5 rounded z-50 text-sm w-full min-w-[150px];
}
.editor.input-clipboard-copy {
@apply top-0 right-0;
}
.input.input-clipboard-copy {
@apply -top-0.5 right-0;
}
.invalid.input-regular,
.invalid.input-regular:hover,
.invalid.input-regular:focus,
@ -389,33 +433,6 @@ body {
@apply flex scale-110 h-5 w-5 items-center align-middle;
}
.input-clipboard-container {
@apply rounded-full absolute flex h-5 w-5;
}
.pw-input.input-clipboard-container {
@apply right-[2.25rem];
}
.no-pw-input.input-clipboard-container {
@apply right-2;
}
.input-clipboard-button {
@apply transition-all rounded-full h-5 w-5 flex items-center align-middle hover:brightness-90;
}
.enabled.input-clipboard-button {
@apply bg-white dark:bg-slate-700;
}
.disabled.input-clipboard-button {
@apply bg-gray-400 dark:bg-gray-800;
}
.input-clipboard-svg {
@apply stroke-gray-800 dark:stroke-gray-300 h-full w-full;
}
/* POPOVER */

View file

@ -8,6 +8,7 @@
"name": "app",
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^10.11.0",
"ace-builds": "^1.24.2",
"flag-icons": "^6.15.0",
"flatpickr": "^4.6.13",
@ -525,6 +526,11 @@
"node": ">= 8"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz",
@ -645,6 +651,89 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
},
"node_modules/@vueuse/core": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz",
"integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.0",
"@vueuse/shared": "10.11.0",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz",
"integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz",
"integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/ace-builds": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.24.2.tgz",

View file

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@vueuse/core": "^10.11.0",
"ace-builds": "^1.24.2",
"flag-icons": "^6.15.0",
"flatpickr": "^4.6.13",

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,13 @@
<script setup>
import {
reactive,
ref,
computed,
defineEmits,
onMounted,
defineProps,
onUnmounted,
} from "vue";
import { useClipboard } from "@vueuse/core";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
@ -16,30 +16,24 @@ import { v4 as uuidv4 } from "uuid";
import "@assets/script/editor/ace.js";
import "@assets/script/editor/theme-dracula.js";
import "@assets/script/editor/theme-dawn.js";
/**
@name Forms/Field/Editor.vue
@description This component is used to create a complete editor field input with error handling and label.
@description This component is used to create a complete editor field with error handling and label.
We can also add popover to display more information.
It is mainly use in forms.
@example
{
id: 'test-input',
value: 'yes',
type: "text",
name: 'test-input',
disabled: false,
required: true,
label: 'Test input',
pattern : "(test)",
inpType: "input",
popovers : [
{
text: "This is a popover text",
iconName: "info",
iconColor: "info",
},
],
}
id: "test-editor",
value: "yes",
name: "test-editor",
disabled: false,
required: true,
pattern: "(test)",
label: "Test editor",
tabId: "1",
columns: { pc: 12, tablet: 12, mobile: 12 },
};
@param {string} [id=uuidv4()] - Unique id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text. @param {string} label
@ -58,6 +52,8 @@ import "@assets/script/editor/theme-dawn.js";
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const { text, copy, copied, isSupported } = useClipboard({ legacy: true });
const props = defineProps({
// id && value && method
id: {
@ -99,6 +95,7 @@ const props = defineProps({
clipboard: {
type: Boolean,
required: false,
default: true,
},
label: {
type: String,
@ -324,24 +321,20 @@ onUnmounted(() => {
:aria-description="$t('inp_editor_desc')"
:id="props.id"
></div>
<div
v-if="props.clipboard && inp.isClipAllow"
:class="[props.type === 'password' ? 'pw-input' : 'no-pw-input']"
class="input-clipboard-container"
>
<div v-if="props.clipboard" class="input-clipboard-container editor">
<button
type="button"
:class="['input-clipboard-button', copied ? 'copied' : 'not-copied']"
:tabindex="contentIndex"
@click.prevent="copyClipboard()"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-clipboard-button"
@click.prevent="copy(editor.value)"
:aria-describedby="`${props.id}-clipboard-text`"
>
<span :id="`${props.id}-clipboard-text`" class="sr-only"
>{{ $t("inp_input_clipboard_desc") }}
</span>
<svg
role="img"
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -352,9 +345,12 @@ onUnmounted(() => {
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"
></path>
</svg>
<div v-if="copied" role="alert" class="editor input-clipboard-copy">
{{ $t("inp_input_clipboard_copied") }}
</div>
</button>
</div>
</div>

View file

@ -5,6 +5,7 @@ import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import { v4 as uuidv4 } from "uuid";
import { useClipboard } from "@vueuse/core";
/**
@name Forms/Field/Input.vue
@ -53,6 +54,8 @@ import { v4 as uuidv4 } from "uuid";
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const { text, copy, copied, isSupported } = useClipboard({ legacy: true });
const props = defineProps({
// id && value && method
id: {
@ -101,6 +104,7 @@ const props = defineProps({
clipboard: {
type: Boolean,
required: false,
default: true,
},
readonly: {
type: Boolean,
@ -152,35 +156,11 @@ const inp = reactive({
const emits = defineEmits(["inp"]);
function copyClipboard() {
if (!inp.clipboard || !inp.isClipAllow) return;
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
if (result.state === "granted" || result.state === "prompt") {
/* write to the clipboard now */
inputEl.select();
inputEl.setSelectionRange(0, 99999); // For mobile devices
// Copy the text inside the text field
return navigator.clipboard.writeText(inputEl.value);
}
});
}
onMounted(() => {
inp.isValid = inputEl.value.checkValidity();
// Clipboard not allowed on http
if (!window.location.href.startsWith("https://")) return;
// Check clipboard permission
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
if (result.state === "granted" || result.state === "prompt") {
inp.isClipAllow = true;
return;
}
});
});
</script>
@ -233,23 +213,23 @@ onMounted(() => {
"
/>
<div
v-if="props.clipboard && inp.isClipAllow"
v-if="props.clipboard"
:class="[props.type === 'password' ? 'pw-input' : 'no-pw-input']"
class="input-clipboard-container"
>
<button
type="button"
:class="['input-clipboard-button', copied ? 'copied' : 'not-copied']"
:tabindex="contentIndex"
@click.prevent="copyClipboard()"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-clipboard-button"
@click.prevent="copy(inp.value)"
:aria-describedby="`${props.id}-clipboard-text`"
>
<span :id="`${props.id}-clipboard-text`" class="sr-only"
>{{ $t("inp_input_clipboard_desc") }}
</span>
<svg
role="img"
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -260,11 +240,15 @@ onMounted(() => {
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"
></path>
</svg>
<div v-if="copied" role="alert" class="input input-clipboard-copy">
{{ $t("inp_input_clipboard_copied") }}
</div>
</button>
</div>
<div v-if="props.type === 'password'" class="input-pw-container">
<button
:tabindex="contentIndex"

View file

@ -95,7 +95,6 @@
"inp_combobox_no_match": "No match found",
"inp_select_dropdown_button_desc": "Toggle hide/show radio group (dropdown) to change value.",
"inp_select_dropdown_desc": "Radio group (dropdown) to change value.",
"inp_input_clipboard_desc": "Copy to clipboard.",
"inp_input_password_desc": "Toggle hide/show password.",
"inp_combobox_placeholder": "Search",
"inp_search_settings": "Search settings",
@ -109,6 +108,8 @@
"inp_search_key": "search key",
"inp_search_key_desc": "Search within the settings key.",
"inp_editor_desc" : "Editor input behaving like a code editor.",
"inp_input_clipboard_copied": "copied to clipboard",
"inp_input_clipboard_desc": "Copy to clipboard on click.",
"action_send": "send {name}",
"action_start": "start {name}",
"action_disable": "disable {name}",