-
Notifications
You must be signed in to change notification settings - Fork 181
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'fabians/fe2-automate-integration' of github.com:speckle…
…systems/speckle-server into fabians/fe2-automate-integration
- Loading branch information
Showing
91 changed files
with
1,770 additions
and
929 deletions.
There are no files selected for viewing
203 changes: 7 additions & 196 deletions
203
packages/frontend-2/components/common/EditableTitleDescription.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,203 +1,14 @@ | ||
<template> | ||
<div> | ||
<!-- Editable Title --> | ||
<div class="flex group"> | ||
<label class="max-w-full overflow-hidden"> | ||
<div class="sr-only">Edit title</div> | ||
<div | ||
:class="titleInputClasses" | ||
class="grow-textarea" | ||
:data-replicated-value="title" | ||
> | ||
<textarea | ||
v-model="title" | ||
maxlength="512" | ||
:class="titleInputClasses" | ||
placeholder="Please enter a valid title" | ||
rows="1" | ||
spellcheck="false" | ||
:disabled="isDisabled" | ||
:cols="title && title.length < 20 ? title.length : undefined" | ||
data-type="title" | ||
@keydown="onInputKeydown" | ||
@blur="onBlur('title')" | ||
@input="onTitleInput" | ||
/> | ||
</div> | ||
</label> | ||
<PencilIcon | ||
v-if="canEdit" | ||
class="shrink-0 ml-2 mt-3 w-4 h-4 opacity-0 group-hover:opacity-100 transition text-foreground-2" | ||
/> | ||
</div> | ||
|
||
<!-- Editable Description --> | ||
<div class="flex gap-x-2 group"> | ||
<label> | ||
<div class="sr-only">Edit description</div> | ||
<div | ||
class="grow-textarea" | ||
:data-replicated-value="description" | ||
:class="descriptionInputClasses" | ||
> | ||
<textarea | ||
v-model="description" | ||
:class="[ | ||
...descriptionInputClasses, | ||
description ? 'focus:min-w-0' : 'min-w-[260px]' | ||
]" | ||
:placeholder="description ? undefined : 'Click here to add a description.'" | ||
:disabled="isDisabled" | ||
rows="1" | ||
spellcheck="false" | ||
maxlength="1000" | ||
:cols=" | ||
description && description?.length < 20 ? description.length : undefined | ||
" | ||
data-type="description" | ||
@keydown="onInputKeydown" | ||
@blur="onBlur('description')" | ||
@input="onDescriptionInput" | ||
/> | ||
</div> | ||
</label> | ||
<div class="shrink-0 ml-2 mt-1 text-foreground-2"> | ||
<PencilIcon | ||
v-if="canEdit" | ||
class="w-4 h-4 opacity-0 group-hover:opacity-100 transition text-foreground-2" | ||
/> | ||
</div> | ||
</div> | ||
<CommonEditableTitle v-model="title" :disabled="disabled" /> | ||
<CommonEditableDescription v-model="description" :disabled="disabled" /> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { PencilIcon } from '@heroicons/vue/20/solid' | ||
import { debounce } from 'lodash-es' | ||
const props = defineProps({ | ||
title: String, | ||
description: String, | ||
canEdit: Boolean, | ||
isDisabled: Boolean | ||
}) | ||
const emit = defineEmits(['update:title', 'update:description']) | ||
const title = ref(props.title) | ||
const description = ref(props.description) | ||
const lastTitleValue = ref(props.title) | ||
const lastDescriptionValue = ref(props.description) | ||
const titleDebounceSaved = ref(false) | ||
const descriptionDebounceSaved = ref(false) | ||
const emitTitle = () => { | ||
lastTitleValue.value = title.value | ||
titleDebounceSaved.value = true | ||
emit('update:title', title.value) | ||
} | ||
const emitDescription = () => { | ||
lastDescriptionValue.value = description.value | ||
descriptionDebounceSaved.value = true | ||
emit('update:description', description.value) | ||
} | ||
const debouncedEmitTitle = debounce(emitTitle, 2000) | ||
const debouncedEmitDescription = debounce(emitDescription, 2000) | ||
const titleInputClasses = computed(() => [ | ||
'h3 tracking-tight border-0 border-b-2 transition focus:border-outline-3 max-w-full', | ||
'p-0 pb-1 bg-transparent border-transparent focus:outline-none focus:ring-0' | ||
]) | ||
const descriptionInputClasses = computed(() => [ | ||
'normal placeholder:text-foreground-2 text-foreground-2 focus:text-foreground', | ||
'border-0 border-b-2 focus:border-outline-3', | ||
'p-0 bg-transparent border-transparent focus:outline-none focus:ring-0' | ||
]) | ||
defineProps<{ | ||
disabled?: boolean | ||
}>() | ||
const onInputKeydown = (e: KeyboardEvent) => { | ||
if (e.target instanceof HTMLElement) { | ||
if (e.target.dataset.type === 'title' && e.code === 'Enter') { | ||
e.preventDefault() | ||
e.target.blur() | ||
} | ||
} | ||
} | ||
const onBlur = (inputType: string) => { | ||
debouncedEmitTitle.cancel() | ||
debouncedEmitDescription.cancel() | ||
if (inputType === 'title' && !titleDebounceSaved.value) { | ||
if (lastTitleValue.value !== title.value) { | ||
lastTitleValue.value = title.value | ||
emitTitle() | ||
} | ||
} else if (inputType === 'description' && !descriptionDebounceSaved.value) { | ||
if (lastDescriptionValue.value !== description.value) { | ||
lastDescriptionValue.value = description.value | ||
emitDescription() | ||
} | ||
} | ||
} | ||
const onTitleInput = () => { | ||
titleDebounceSaved.value = false | ||
debouncedEmitTitle() | ||
} | ||
const onDescriptionInput = () => { | ||
descriptionDebounceSaved.value = false | ||
debouncedEmitDescription() | ||
} | ||
watch( | ||
() => props.title, | ||
(newVal) => { | ||
title.value = newVal | ||
} | ||
) | ||
watch( | ||
() => props.description, | ||
(newVal) => { | ||
description.value = newVal | ||
} | ||
) | ||
const description = defineModel<string>('description', { required: true }) | ||
const title = defineModel<string>('title', { required: true }) | ||
</script> | ||
|
||
<style scoped> | ||
/** more info: https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ */ | ||
.grow-textarea { | ||
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ | ||
display: grid; | ||
} | ||
.grow-textarea::after { | ||
/* Note the weird space! Needed to preventy jumpy behavior */ | ||
content: attr(data-replicated-value) ' '; | ||
/* This is how textarea text behaves */ | ||
white-space: pre-wrap; | ||
/* Hidden from view, clicks, and screen readers */ | ||
visibility: hidden; | ||
} | ||
.grow-textarea > textarea { | ||
/* You could leave this, but after a user resizes, then it ruins the auto sizing */ | ||
resize: none; | ||
/* Firefox shows scrollbar on growth, you can hide like this. */ | ||
overflow: hidden; | ||
} | ||
.grow-textarea > textarea, | ||
.grow-textarea::after { | ||
/* Place on top of each other - has to have the same styling as the textarea! */ | ||
grid-area: 1 / 1 / 2 / 2; | ||
} | ||
</style> |
64 changes: 64 additions & 0 deletions
64
packages/frontend-2/components/common/editable/Description.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<template> | ||
<div class="flex gap-x-2 group"> | ||
<label> | ||
<div class="sr-only">Edit description</div> | ||
<div | ||
class="grow-textarea" | ||
:data-replicated-value="visibleDescription" | ||
:class="descriptionInputClasses" | ||
> | ||
<textarea | ||
name="Description" | ||
:class="[ | ||
...descriptionInputClasses, | ||
visibleDescription ? 'focus:min-w-0' : 'min-w-[260px]' | ||
]" | ||
:placeholder=" | ||
visibleDescription ? undefined : 'Click here to add a description.' | ||
" | ||
:disabled="disabled" | ||
rows="1" | ||
spellcheck="false" | ||
maxlength="1000" | ||
:cols=" | ||
visibleDescription && visibleDescription?.length < 20 | ||
? visibleDescription.length | ||
: undefined | ||
" | ||
data-type="description" | ||
:value="visibleDescription" | ||
v-on="on" | ||
/> | ||
</div> | ||
</label> | ||
<div class="shrink-0 ml-2 mt-1 text-foreground-2"> | ||
<PencilIcon | ||
v-if="!disabled" | ||
class="w-4 h-4 opacity-0 group-hover:opacity-100 transition text-foreground-2" | ||
/> | ||
</div> | ||
</div> | ||
</template> | ||
<script setup lang="ts"> | ||
import { PencilIcon } from '@heroicons/vue/20/solid' | ||
import { useDebouncedTextInput } from '@speckle/ui-components' | ||
defineProps<{ | ||
disabled?: boolean | ||
}>() | ||
const description = defineModel<string>({ required: true }) | ||
const { on, bind } = useDebouncedTextInput({ | ||
model: description, | ||
submitOnEnter: false, | ||
debouncedBy: 2000, | ||
isBasicHtmlInput: true | ||
}) | ||
const visibleDescription = computed(() => bind.modelValue.value) | ||
const descriptionInputClasses = computed(() => [ | ||
'normal placeholder:text-foreground-2 text-foreground-2 focus:text-foreground', | ||
'border-0 border-b-2 focus:border-outline-3', | ||
'p-0 bg-transparent border-transparent focus:outline-none focus:ring-0' | ||
]) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<template> | ||
<div class="flex group"> | ||
<label class="max-w-full overflow-hidden"> | ||
<div class="sr-only">Edit title</div> | ||
<div | ||
:class="titleInputClasses" | ||
class="grow-textarea" | ||
:data-replicated-value="visibleTitle" | ||
> | ||
<textarea | ||
name="Title" | ||
maxlength="512" | ||
:class="titleInputClasses" | ||
placeholder="Please enter a valid title" | ||
rows="1" | ||
spellcheck="false" | ||
:disabled="disabled" | ||
:cols=" | ||
visibleTitle && visibleTitle.length < 20 ? visibleTitle.length : undefined | ||
" | ||
data-type="title" | ||
:value="visibleTitle" | ||
v-on="on" | ||
/> | ||
</div> | ||
</label> | ||
<PencilIcon v-if="!disabled" :class="pencilClasses" /> | ||
</div> | ||
</template> | ||
<script setup lang="ts"> | ||
import { PencilIcon } from '@heroicons/vue/20/solid' | ||
import { useDebouncedTextInput } from '@speckle/ui-components' | ||
const props = defineProps<{ | ||
disabled?: boolean | ||
customClasses?: { | ||
input?: string | ||
pencil?: string | ||
} | ||
}>() | ||
const title = defineModel<string>({ required: true }) | ||
const { on, bind } = useDebouncedTextInput({ | ||
model: title, | ||
debouncedBy: 2000, | ||
isBasicHtmlInput: true, | ||
submitOnEnter: true | ||
}) | ||
const visibleTitle = computed(() => bind.modelValue.value) | ||
const titleInputClasses = computed(() => { | ||
const classParts = [ | ||
'border-0 border-b-2 transition focus:border-outline-3 max-w-full', | ||
'p-0 pb-1 bg-transparent border-transparent focus:outline-none focus:ring-0' | ||
] | ||
if (props.customClasses?.input) { | ||
classParts.push(props.customClasses.input) | ||
} else { | ||
classParts.push('h3 tracking-tight') | ||
} | ||
return classParts.join(' ') | ||
}) | ||
const pencilClasses = computed(() => { | ||
const classParts = [ | ||
'shrink-0 opacity-0 group-hover:opacity-100 transition text-foreground-2' | ||
] | ||
if (props.customClasses?.pencil) { | ||
classParts.push(props.customClasses.pencil) | ||
} else { | ||
classParts.push('ml-2 mt-3 w-4 h-4') | ||
} | ||
return classParts.join(' ') | ||
}) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.