mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-09 16:01:30 +08:00
Edit number/text column inside item card [SCI-9600][SCI-9696] (#6546)
* Refactor Repository Text and Number value component [SCI-9600] - Refactor text and number components. - Add number validation. * Create a Textarea standalone component [SCI-9600][SCI-9618]
This commit is contained in:
parent
a0da544e27
commit
dbff182314
3 changed files with 209 additions and 124 deletions
135
app/javascript/vue/repository_item_sidebar/Textarea.vue
Normal file
135
app/javascript/vue/repository_item_sidebar/Textarea.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<textarea v-if="editing"
|
||||||
|
ref="textareaRef"
|
||||||
|
class="leading-5 inline-block outline-none border-solid font-normal border-[1px] box-content
|
||||||
|
overflow-x-hidden overflow-y-auto resize-none rounded px-4 py-2 w-[calc(100%-2rem)]
|
||||||
|
border-sn-science-blue"
|
||||||
|
:class="{
|
||||||
|
'max-h-[4rem]': collapsed,
|
||||||
|
'max-h-[40rem]': !collapsed
|
||||||
|
}"
|
||||||
|
:placeholder="i18n.t('repositories.item_card.repository_number_value.placeholder')"
|
||||||
|
v-model="value"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
@blur="handleBlur" />
|
||||||
|
<div v-else
|
||||||
|
:ref="unEditableRef"
|
||||||
|
class="grid box-content sci-cursor-edit font-normal border-solid px-4 py-2 border-sn-light-grey rounded
|
||||||
|
leading-5 border outline-none hover:border-sn-sleepy-grey overflow-y-auto whitespace-pre-line
|
||||||
|
w-[calc(100%-2rem)]"
|
||||||
|
:class="{ 'max-h-[4rem]': collapsed,
|
||||||
|
'max-h-[40rem]': !collapsed, }"
|
||||||
|
@click="enableEdit">
|
||||||
|
<span v-html="value || noContentPlaceholder" ></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Textarea",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: '',
|
||||||
|
editing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
expandable: { type: Boolean, required: true },
|
||||||
|
collapsed: { type: Boolean, required: true },
|
||||||
|
initialValue: String,
|
||||||
|
noContentPlaceholder: String,
|
||||||
|
decimals: { type: Number, default: null },
|
||||||
|
unEditableRef: { type: String, required: true },
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.toggleExpandableState();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUpdate() {
|
||||||
|
if (!this.$refs.textareaRef) return;
|
||||||
|
|
||||||
|
if (this.decimals !== null) this.validateNumberInput();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
initialValue: {
|
||||||
|
handler() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
this.toggleExpandableState();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
editing() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.editing) {
|
||||||
|
this.setCaretAtEnd();
|
||||||
|
this.refreshTextareaHeight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleExpandableState();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEdit() {
|
||||||
|
return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.$refs.textareaRef.blur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleBlur() {
|
||||||
|
this.editing = false;
|
||||||
|
this.toggleExpandableState();
|
||||||
|
this.$emit('update', this.value);
|
||||||
|
},
|
||||||
|
toggleExpandableState() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.$refs[this.unEditableRef]) return;
|
||||||
|
|
||||||
|
const maxCollapsedHeight = '80';
|
||||||
|
const scrollHeight = this.$refs[this.unEditableRef].scrollHeight;
|
||||||
|
const expandable = scrollHeight > maxCollapsedHeight;
|
||||||
|
this.$emit('toggleExpandableState', expandable);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enableEdit(e) {
|
||||||
|
if (e && $(e.target).hasClass('atwho-user-popover')) return;
|
||||||
|
if (e && $(e.target).hasClass('sa-link')) return;
|
||||||
|
if (e && $(e.target).parent().hasClass('atwho-inserted')) return;
|
||||||
|
|
||||||
|
this.editing = true;
|
||||||
|
},
|
||||||
|
refreshTextareaHeight() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.editing) return;
|
||||||
|
const textarea = this.$refs.textareaRef;
|
||||||
|
textarea.style.height = '0px';
|
||||||
|
// 16px is the height of the textarea's line
|
||||||
|
textarea.style.height = textarea.scrollHeight - 16 + 'px';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCaretAtEnd() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.editing) return;
|
||||||
|
|
||||||
|
this.$refs.textareaRef.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validateNumberInput() {
|
||||||
|
const regexp = this.decimals === 0 ? /[^0-9]/g : /[^0-9.]/g;
|
||||||
|
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${this.decimals}})?`);
|
||||||
|
let value = this.value;
|
||||||
|
value = value.replace(regexp, '');
|
||||||
|
value = value.match(decimalsRegex)[0];
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="repository-number-value-wrapper"
|
<div id="repository-number-value-wrapper"
|
||||||
class="flex flex-col min-h-[46px] h-auto gap-[6px]"
|
class="flex flex-col min-h-[46px] h-auto gap-[6px]">
|
||||||
>
|
|
||||||
<div class="font-inter text-sm font-semibold leading-5 flex justify-between">
|
<div class="font-inter text-sm font-semibold leading-5 flex justify-between">
|
||||||
<div class="truncate" :class="{ 'w-4/5': expandable }" :title="colName">{{ colName }}</div>
|
<div class="truncate" :class="{ 'w-4/5': expandable }" :title="colName">{{ colName }}</div>
|
||||||
<div @click="toggleCollapse"
|
<div @click="toggleCollapse"
|
||||||
|
|
@ -14,90 +13,76 @@
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="canEdit">
|
||||||
<inline-edit
|
<text-area :initialValue="colVal.toString()"
|
||||||
v-if="permissions?.can_manage && !inArchivedRepositoryRow"
|
:noContentPlaceholder="noContentPlaceholder"
|
||||||
ref="numberRef"
|
:decimals="decimals"
|
||||||
:value="colVal"
|
:unEditableRef="`numberRef`"
|
||||||
:placeholder="i18n.t('repositories.item_card.repository_number_value.placeholder')"
|
:expandable="expandable"
|
||||||
:noContentPlaceholder="i18n.t('repositories.item_card.repository_number_value.no_number')"
|
:collapsed="collapsed"
|
||||||
:allowBlank="true"
|
@toggleExpandableState="toggleExpandableState"
|
||||||
:smartAnnotation="false"
|
@update="update" />
|
||||||
:attributeName="`${colName} `"
|
</div>
|
||||||
:allowNewLine="false"
|
|
||||||
:singleLine="false"
|
|
||||||
:expandable="true"
|
|
||||||
:collapsed="collapsed"
|
|
||||||
@editingEnabled="editing = true"
|
|
||||||
@editingDisabled="editing = false"
|
|
||||||
@update="update"
|
|
||||||
></inline-edit>
|
|
||||||
<div v-else-if="colVal"
|
<div v-else-if="colVal"
|
||||||
ref="numberRef"
|
ref="numberRef"
|
||||||
class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 min-h-[20px] overflow-y-auto"
|
class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 min-h-[20px] overflow-y-auto"
|
||||||
:class="{
|
:class="{
|
||||||
'max-h-[4rem]': collapsed,
|
'max-h-[4rem]': collapsed,
|
||||||
'max-h-[40rem]': !collapsed
|
'max-h-[40rem]': !collapsed
|
||||||
}"
|
}">
|
||||||
>
|
|
||||||
{{ colVal }}
|
{{ colVal }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else
|
<div v-else
|
||||||
class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
|
class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
|
||||||
{{ i18n.t("repositories.item_card.repository_number_value.no_number") }}
|
{{ i18n.t("repositories.item_card.repository_number_value.no_number") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import InlineEdit from "../../shared/inline_edit.vue";
|
|
||||||
import repositoryValueMixin from "./mixins/repository_value.js";
|
import repositoryValueMixin from "./mixins/repository_value.js";
|
||||||
|
import Textarea from "../Textarea.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RepositoryNumberValue",
|
name: "RepositoryNumberValue",
|
||||||
mixins: [repositoryValueMixin],
|
mixins: [repositoryValueMixin],
|
||||||
components: {
|
components: {
|
||||||
"inline-edit": InlineEdit
|
'text-area': Textarea,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expandable: false,
|
||||||
|
collapsed: true,
|
||||||
|
numberValue: '',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
data_type: String,
|
data_type: String,
|
||||||
inArchivedRepository: Boolean,
|
|
||||||
colId: Number,
|
colId: Number,
|
||||||
colName: String,
|
colName: String,
|
||||||
colVal: String,
|
colVal: String,
|
||||||
permissions: null,
|
permissions: null,
|
||||||
inArchivedRepositoryRow: Boolean,
|
inArchivedRepositoryRow: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
created() {
|
||||||
return {
|
// constants
|
||||||
expandable: false,
|
this.noContentPlaceholder = this.i18n.t("repositories.item_card.repository_number_value.no_number");
|
||||||
collapsed: true
|
this.decimals = Number(document.getElementById(`${this.colId}`).dataset['metadataDecimals']) || 0;
|
||||||
};
|
|
||||||
},
|
},
|
||||||
mounted() {
|
computed: {
|
||||||
this.$nextTick(() => {
|
canEdit() {
|
||||||
this.toggleExpandableState()
|
return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
|
||||||
});
|
}
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.toggleExpandableState();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
if (this.expandable) {
|
if (!this.expandable) return;
|
||||||
this.collapsed = !this.collapsed;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleExpandableState() {
|
|
||||||
if (!this.$refs.numberRef) return;
|
|
||||||
|
|
||||||
const offsetHeight = this.$refs.numberRef.offsetHeight;
|
this.collapsed = !this.collapsed;
|
||||||
const scrollHeight = this.$refs.numberRef.scrollHeight;
|
},
|
||||||
this.expandable = scrollHeight > offsetHeight;
|
toggleExpandableState(expandable) {
|
||||||
}
|
this.expandable = expandable;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -13,59 +13,49 @@
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<inline-edit
|
<div v-if="canEdit">
|
||||||
v-if="permissions?.can_manage && !inArchivedRepositoryRow"
|
<text-area :initialValue="colVal?.edit"
|
||||||
ref="textRef"
|
:noContentPlaceholder="noContentPlaceholder"
|
||||||
:value="colVal?.edit"
|
:unEditableRef="`textRef`"
|
||||||
:placeholder="i18n.t('repositories.item_card.repository_text_value.placeholder')"
|
:expandable="expandable"
|
||||||
:noContentPlaceholder="i18n.t('repositories.item_card.repository_text_value.no_text')"
|
:collapsed="collapsed"
|
||||||
:allowBlank="true"
|
@toggleExpandableState="toggleExpandableState"
|
||||||
:smartAnnotation="false"
|
@update="update" />
|
||||||
:attributeName="`${colName} `"
|
</div>
|
||||||
:allowNewLine="false"
|
<div v-else-if="colVal?.edit"
|
||||||
:singleLine="false"
|
ref="textRef"
|
||||||
:expandable="true"
|
class="text-sn-dark-grey box-content text-sm font-normal leading-5 overflow-y-auto px-4 py-2
|
||||||
:collapsed="collapsed"
|
border-sn-light-grey border border-solid rounded w-[calc(100%-2rem)]]"
|
||||||
@editingEnabled="editing = true"
|
:class="{
|
||||||
@editingDisabled="editing = false"
|
'max-h-[4rem]': collapsed,
|
||||||
@update="update"
|
'max-h-[40rem]': !collapsed
|
||||||
></inline-edit>
|
}"
|
||||||
<div v-else-if="colVal?.edit"
|
>
|
||||||
ref="textRef"
|
{{ colVal?.edit }}
|
||||||
class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 overflow-y-auto px-4 py-2 border-sn-light-grey border border-solid rounded"
|
</div>
|
||||||
:class="{
|
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5 px-4 py-2 w-[calc(100%-2rem)]]">
|
||||||
'max-h-[4rem]': collapsed,
|
{{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
|
||||||
'max-h-[40rem]': !collapsed
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ colVal?.edit }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
|
|
||||||
{{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import InlineEdit from "../../shared/inline_edit.vue";
|
|
||||||
import repositoryValueMixin from "./mixins/repository_value.js";
|
import repositoryValueMixin from "./mixins/repository_value.js";
|
||||||
|
import Textarea from "../Textarea.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RepositoryTextValue",
|
name: "RepositoryTextValue",
|
||||||
mixins: [repositoryValueMixin],
|
mixins: [repositoryValueMixin],
|
||||||
components: {
|
components: {
|
||||||
"inline-edit": InlineEdit
|
'text-area': Textarea,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
edit: null,
|
|
||||||
view: null,
|
|
||||||
contentExpanded: false,
|
|
||||||
expandable: false,
|
expandable: false,
|
||||||
collapsed: true
|
collapsed: true,
|
||||||
}
|
textValue: '',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
data_type: String,
|
data_type: String,
|
||||||
|
|
@ -75,48 +65,23 @@ export default {
|
||||||
permissions: null,
|
permissions: null,
|
||||||
inArchivedRepositoryRow: Boolean,
|
inArchivedRepositoryRow: Boolean,
|
||||||
},
|
},
|
||||||
mounted() {
|
created() {
|
||||||
this.$nextTick(() => {
|
// constants
|
||||||
if (this.$refs.textRef) {
|
this.noContentPlaceholder = this.i18n.t("repositories.item_card.repository_text_value.no_text");
|
||||||
const textHeight = this.$refs.textRef.scrollHeight
|
|
||||||
this.expandable = textHeight > 60 // 60px
|
|
||||||
}
|
|
||||||
this.toggleExpandableState();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
watch: {
|
computed: {
|
||||||
editing() {
|
canEdit() {
|
||||||
if (this.editing) return;
|
return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
|
||||||
|
|
||||||
this.toggleExpandableState();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
if (this.expandable) {
|
if (!this.expandable) return;
|
||||||
this.collapsed = !this.collapsed;
|
|
||||||
}
|
this.collapsed = !this.collapsed;
|
||||||
},
|
},
|
||||||
toggleExpandableState() {
|
toggleExpandableState(expandable) {
|
||||||
if (!this.$refs.textRef) return;
|
this.expandable = expandable;
|
||||||
let offsetHeight;
|
|
||||||
let scrollHeight;
|
|
||||||
|
|
||||||
if (Object.keys(this.$refs.textRef.$refs).length > 0) {
|
|
||||||
const keys = Object.keys(this.$refs.textRef.$refs)
|
|
||||||
keys.forEach((ref) => {
|
|
||||||
if (this.$refs.textRef.$refs[ref] !== undefined) {
|
|
||||||
offsetHeight = this.$refs.textRef.$refs[ref].offsetHeight;
|
|
||||||
scrollHeight = this.$refs.textRef.$refs[ref].scrollHeight;
|
|
||||||
this.expandable = scrollHeight > offsetHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetHeight = this.$refs.textRef.offsetHeight;
|
|
||||||
scrollHeight = this.$refs.textRef.scrollHeight;
|
|
||||||
this.expandable = scrollHeight > offsetHeight;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue