mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
add vueuse useClipboard
This commit is contained in:
parent
76ccbf333e
commit
b223644ac5
7 changed files with 178 additions and 90 deletions
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
89
vuejs/client/package-lock.json
generated
89
vuejs/client/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue