Rework inline edit vue component UI [SCI-9016]

This commit is contained in:
Ivan Kljun 2023-09-01 17:18:13 +02:00
parent 51227b5c65
commit 40638dbb63
12 changed files with 154 additions and 172 deletions

View file

@ -123,14 +123,14 @@
}
.authors-data {
align-items: center;
align-items: baseline;
display: flex;
margin-top: -12px;
min-height: 36px;
.authors-list {
flex-basis: calc(100% - 90px);
flex-grow: 1;
position: relative;
max-width: calc(100% - 90px);
line-height: 32px;
margin-left: 8px;
font-weight: bold;
@ -260,10 +260,12 @@
.protocol-content {
.protocol-name {
display: inline-block;
font-size: 1.7em;
font-weight: bold;
margin-bottom: 32px;
margin-top: 28px;
position: relative;
}
.repository-new-step {

View file

@ -26,8 +26,10 @@
grid-template-columns: max-content auto;
.step-checklist-text {
display: inline-block;
margin-top: -.1em;
width: 100%;
position: relative;
width: fit-content;
}
&:hover.done .step-checklist-text {
@ -42,7 +44,7 @@
border-radius: 50%;
height: 4px;
margin-right: .5em;
margin-top: 1em;
margin-top: .4em;
width: 4px;
}

View file

@ -1,95 +1,11 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
.sci-inline-edit {
align-items: flex-start;
display: flex;
transition: .4s $timing-function-sharp border;
&.editing .sci-inline-edit__content {
.sci-inline-edit__content {
padding-top: 0;
}
}
.sci-inline-edit__content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin-left: -.25em;
margin-right: .5rem;
min-height: 30px;
padding-left: .25em;
position: relative;
textarea,
.sci-inline-edit__view {
line-height: 26px;
min-height: 30px;
padding: .1em .2em;
}
.sci-inline-edit__view {
border: 1px solid transparent;
cursor: pointer;
white-space: pre-wrap;
width: 100%;
&.blank {
color: $color-silver-chalice;
}
&.hover-border:hover {
border: 1px solid $color-silver;
border-radius: 3px;
}
}
textarea {
background-color: $color-white;
border: 1px solid $brand-focus;
border-radius: 4px;
outline: none;
overflow: hidden;
width: 100%;
&:focus {
outline: none;
}
}
.sci-inline-edit__error {
bottom: -16px;
color: $brand-danger;
font-size: 12px;
font-weight: normal;
line-height: 12px;
position: absolute;
}
}
&.editing {
margin-top: 0;
.sci-inline-edit__content {
&.error {
border-color: $brand-danger;
margin-bottom: 16px;
textarea {
border-color: $brand-danger;
}
}
}
.sci-inline-edit__control {
margin: 0 .25em;
max-height: 30px;
max-width: 30px;
&.btn-disabled {
background: $color-silver-chalice;
color: $color-concrete;
}
}
}
.sci-cursor-edit {
cursor: url("/images/icon_small/edit.svg") 0 16, auto;
}
.inline-edit-placeholder:empty:before {
content: attr(placeholder);
display: block;
}

View file

@ -18,9 +18,10 @@
align-items: baseline;
border-radius: 4px;
display: flex;
flex-basis: 80%;
flex-basis: 100%;
position: relative;
padding-top: .4em;
gap: .5em;
.step-name-edit-icon {
background: linear-gradient(90deg,
@ -65,7 +66,6 @@
.step-name-container {
align-self: baseline;
flex-grow: 1;
font-size: 16px;
font-weight: bold;
@ -221,7 +221,7 @@
border-radius: 4px;
display: flex;
min-height: 40px;
overflow: hidden;
// overflow: hidden;
position: relative;
&.editing-name {
@ -242,8 +242,8 @@
.step-element-name {
font-weight: bold;
width: 100%;
margin-bottom: 2em;
position: relative;
.step-element-number {
display: inline-block;

View file

@ -10,9 +10,9 @@
:characterMinLimit="2"
:allowBlank="false"
:attributeName="i18n.t('label_templates.show.name_error_prefix')"
:placeholder="i18n.t('label_templates.show.name_placeholder')"
:autofocus="editingName"
:editOnload="newLabel"
:customClasses="['hover-border']"
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
@update="updateName"
@ -35,7 +35,6 @@
:attributeName="i18n.t('label_templates.show.description_error_prefix')"
:placeholder="i18n.t('label_templates.show.description_placeholder')"
:autofocus="editingDescription"
:customClasses="['hover-border']"
@editingEnabled="editingDescription = true"
@editingDisabled="editingDescription = false"
@update="updateDescription"

View file

@ -71,7 +71,6 @@
:placeholder="i18n.t('my_modules.protocols.protocol_status_bar.enter_name')"
:allowBlank="!inRepository"
:attributeName="`${i18n.t('Protocol')} ${i18n.t('name')}`"
:customClasses="['hover-border']"
@update="updateName"
/>
<span v-else>

View file

@ -56,7 +56,8 @@
:value="protocol.attributes.authors"
:placeholder="i18n.t('protocols.header.add_authors')"
:allowBlank="true"
:attributeName="`${i18n.t('Protocol')} ${i18n.t('authors')}`"
:attributeName="`${i18n.t('Protocol')} ${i18n.t('protocols.header.authors_list')}`"
:characterLimit="10000"
@update="updateAuthors"
/>
</span>

View file

@ -44,6 +44,7 @@
:allowBlank="false"
:attributeName="`${i18n.t('Step')} ${i18n.t('name')}`"
:autofocus="editingName"
:timestamp="i18n.t('protocols.steps.timestamp', { date: step.attributes.created_at, user: step.attributes.created_by })"
:placeholder="i18n.t('protocols.steps.placeholder')"
:defaultValue="i18n.t('protocols.steps.default_name')"
@editingEnabled="editingName = true"
@ -52,9 +53,6 @@
@update="updateName"
/>
</div>
<button v-if="urls.update_url && !editingName" class="step-name-edit-icon btn btn-xs icon-btn btn-light my-1.5" @click="editingName = true">
<i class="sn-icon sn-icon-edit"></i>
</button>
</div>
<div class="elements-actions-container">
<input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple />
@ -158,7 +156,6 @@
</div>
<div class="collapse in" :id="'stepBody' + step.id">
<div class="step-elements">
<div class="step-timestamp">{{ i18n.t('protocols.steps.timestamp', {date: step.attributes.created_at, user: step.attributes.created_by}) }}</div>
<template v-for="(element, index) in orderedElements">
<component
:is="elements[index].attributes.orderable_type"
@ -260,6 +257,7 @@
reordering: false,
isCollapsed: false,
editingName: false,
inlineEditError: null,
wellPlateOptions: [
{ label: 'protocols.steps.insert.well_plate_options.32_x_48', dimensions: [32, 48] },
{ label: 'protocols.steps.insert.well_plate_options.16_x_24', dimensions: [16, 24] },

View file

@ -12,7 +12,7 @@
:value="element.attributes.orderable.name"
:sa_value="element.attributes.orderable.sa_name"
:characterLimit="10000"
:placeholder="i18n.t('protocols.steps.checklist.checklist_name')"
:placeholder="i18n.t('protocols.steps.checklist.placeholder')"
:allowBlank="false"
:autofocus="editingName"
:smartAnnotation="true"
@ -173,6 +173,7 @@
this.$emit('update', this.element, skipRequest);
},
postItem(item, callback) {
console.log(this.element.attributes.orderable.urls.create_item_url)
$.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => {
this.checklistItems.splice(
result.data.attributes.position,
@ -181,8 +182,8 @@
);
if(callback) callback();
}).fail((xhr) => {
this.setFlashErrors(xhr.responseJSON.errors)
}).fail((e) => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
});
this.update();

View file

@ -1,32 +1,41 @@
<template>
<div class="sci-inline-edit" :class="{ 'editing': editing }" tabindex="0" @keyup.enter="enableEdit($event)">
<div class="sci-inline-edit__content" :class="{ 'error': error }">
<textarea
ref="input"
rows="1"
v-if="editing"
:placeholder="placeholder"
v-model="newValue"
@input="handleInput"
@keydown="handleKeypress"
@paste="handlePaste"
@blur="handleBlur"
@keyup.escape="cancelEdit"
></textarea>
<div v-else-if="smartAnnotation" @click="enableEdit($event)" class="sci-inline-edit__view" v-html="sa_value || placeholder" :class="{ 'blank': isBlank }"></div>
<div v-else @click="enableEdit($event)" class="sci-inline-edit__view" :class="[isBlank ? 'blank' : '', ...customClasses]">{{newValue || placeholder}}</div>
<div v-if="editing && error" class="sci-inline-edit__error">
{{ error }}
</div>
<div class="flex flex-col">
<span
v-if="editing"
ref="input"
contenteditable="true"
class="outline-none p-0 pb-2 border-0 border-solid border-b w-fit"
:class="{ 'inline-edit-placeholder text-sn-grey caret-black': isBlank, 'border-sn-delete-red': error, 'border-sn-science-blue': !error }"
:placeholder="placeholder"
@input="handleInput"
@keydown="handleKeypress"
@paste="handlePaste"
@blur="handleBlur"
@keyup.escape="cancelEdit"
@focus="setCaretAtEnd"
></span>
<div
v-else-if="smartAnnotation"
class="sci-cursor-edit"
:class="{ 'blank': isBlank }"
v-html="sa_value || placeholder"
@click="enableEdit($event)"
></div>
<div
v-else
class="sci-cursor-edit outline-none"
:class="{ 'text-sn-grey': isBlank }"
@click="enableEdit($event)"
>
{{newValue || placeholder}}
</div>
<div
class="mt-2 whitespace-nowrap text-xs font-normal"
:class="{'text-sn-delete-red': editing && error}"
>
{{ editing && error ? error : timestamp }}
</div>
<template v-if="editing">
<div :class="{ 'btn-primary': !error, 'btn-disabled': error }" class="sci-inline-edit__control btn btn-sm icon-btn" @click="update">
<i class="sn-icon sn-icon-check"></i>
</div>
<div class="sci-inline-edit__control btn btn-light btn-sm icon-btn" @mousedown="cancelEdit">
<i class="sn-icon sn-icon-close"></i>
</div>
</template>
</div>
</template>
@ -42,6 +51,7 @@
attributeName: { type: String, required: true },
characterLimit: { type: Number },
characterMinLimit: { type: Number },
timestamp: { type: String},
placeholder: { type: String },
autofocus: { type: Boolean, default: false },
saveOnEnter: { type: Boolean, default: true },
@ -49,8 +59,7 @@
multilinePaste: { type: Boolean, default: false },
smartAnnotation: { type: Boolean, default: false },
editOnload: { type: Boolean, default: false },
defaultValue: { type: String, default: '' },
customClasses: { type: Array, default: () => [] }
defaultValue: { type: String, default: '' }
},
data() {
return {
@ -60,7 +69,7 @@
}
},
mixins: [UtilsMixin],
created( ){
created() {
this.newValue = this.value || '';
},
mounted() {
@ -70,17 +79,23 @@
}
},
watch: {
newValue() {
if (this.newValue.length === 0 && this.editing) {
this.focus();
this.setCaretPosition();
}
},
autofocus() {
this.handleAutofocus();
},
value() {
this.newValue = this.value;
}
},
computed: {
isBlank() {
return this.newValue.length === 0
},
isContentDefault() {
return this.$refs ? this.$refs.input.innerText === this.defaultValue : false;
},
error() {
if(!this.allowBlank && this.isBlank) {
return this.i18n.t('inline_edit.errors.blank', { attribute: this.attributeName })
@ -118,7 +133,6 @@
},
handleBlur() {
if ($('.atwho-view:visible').length) return;
if (this.allowBlank || !this.isBlank) {
this.$nextTick(this.update);
} else {
@ -128,21 +142,47 @@
focus() {
this.$nextTick(() => {
if (!this.$refs.input) return;
this.$refs.input.focus();
this.resize();
});
},
// Fixing Firefox specific caret placement issue
setCaretPosition() {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(this.$refs.input, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
},
setCaretAtEnd() {
if (this.isBlank || this.isContentDefault) return;
const el = this.$refs.input;
let range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
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;
this.focus();
this.$nextTick(() => {
if (this.$refs.input.value === this.defaultValue) {
this.$refs.input.select();
this.focus();
this.$refs.input.innerText = this.newValue;
// Select whole content if it is default
if (this.isContentDefault) {
let range = document.createRange();
range.selectNodeContents(this.$refs.input);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
if (this.smartAnnotation) {
SmartAnnotation.init($(this.$refs.input), false);
@ -156,40 +196,57 @@
this.$emit('editingDisabled');
},
handlePaste(e) {
e.preventDefault();
this.dirty = true;
if (!this.multilinePaste) return;
let lines = (e.originalEvent || e).clipboardData.getData('text/plain').split(/[\n\r]/);
lines = lines.filter((l) => l).map((l) => l.trim());
const clipboardData = (e.clipboardData || window.clipboardData).getData("text");
let lines = clipboardData.split(/[\n\r]/).filter((l) => l).map((l) => l.trim());
const selection = window.getSelection();
if (!selection.rangeCount) return;
if (lines.length > 1) {
this.newValue = lines[0];
this.$emit('multilinePaste', lines);
this.update();
}
selection.deleteFromDocument();
let textNode = document.createTextNode(lines[0]);
selection.getRangeAt(0).insertNode(textNode);
let range = document.createRange();
range.setStart(textNode, textNode.length);
range.setEnd(textNode, textNode.length);
this.$nextTick(() => {
this.newValue = e.target.textContent;
selection.removeAllRanges();
selection.addRange(range);
// Handle multi-line paste
if (this.multilinePaste && lines.length > 1) {
this.$emit('multilinePaste', lines);
this.update();
}
});
},
handleInput() {
handleInput(e) {
this.dirty = true;
if (!this.allowNewLine) {
this.newValue = this.newValue.replace(/^[\n\r]+|[\n\r]+$/g, '');
}
this.$nextTick(this.resize);
const sel = document.getSelection();
const offset = sel.anchorOffset;
this.newValue = this.allowNewLine ? e.target.textContent : e.target.textContent.replace(/^[\n\r]+|[\n\r]+$/g, '');
sel.collapse(sel.anchorNode, offset);
},
handleKeypress(e) {
if (e.key == 'Escape') {
this.cancelEdit();
} else if (e.key == 'Enter' && this.saveOnEnter) {
e.preventDefault()
this.update();
} else {
if (!this.error) this.$emit('error-cleared');
this.dirty = true;
}
},
resize() {
if (!this.$refs.input) return;
this.$refs.input.style.height = "auto";
this.$refs.input.style.height = (this.$refs.input.scrollHeight) + "px";
},
update() {
if (!this.dirty && !this.isBlank) {
this.editing = false;
@ -198,8 +255,8 @@
if(this.error) return;
if(!this.$refs.input) return;
this.newValue = this.$refs.input.value // Fix for smart annotation
this.newValue = this.newValue.trim();
this.newValue = this.$refs.input.innerText.trim() // Fix for smart annotation
this.editing = false;
this.$emit('editingDisabled');
this.$emit('update', this.newValue);

View file

@ -989,6 +989,7 @@ en:
show:
breadcrumb_index: 'Label templates'
name_error_prefix: 'Label template name'
name_placeholder: 'Enter label template name'
description_error_prefix: 'Label template description'
description_title: 'Template description'
description_placeholder: 'Enter the template description (optional)'
@ -2915,6 +2916,7 @@ en:
added_by: "Created by:"
keywords: "Keywords:"
authors: "Authors:"
authors_list: "authors list"
no_authors: "No authors"
add_authors: "+ Add authors"
add_keywords: "+ Add keywords"
@ -3115,6 +3117,7 @@ en:
error: "Table name can't be empty"
table_name: "Table name"
checklist:
placeholder: 'Enter checklist name'
default_name: 'Checklist %{position}'
checklist_name: "Checklist name"
empty_checklist: 'Doesnt contain any checklist items'

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.024 14.9594H2.1248L12.2624 4.65691L11.1616 3.53821L1.024 13.8407V14.9594ZM14.464 3.90244L11.904 1.32683L12.7232 0.494309C13.0304 0.16477 13.4144 0 13.8752 0C14.336 0 14.72 0.16477 15.0272 0.494309L15.6928 1.17073C15.8976 1.36152 16 1.60434 16 1.89919C16 2.19404 15.9061 2.43686 15.7184 2.62764L14.464 3.90244ZM13.7216 4.65691L2.56 16H0V13.3984L11.1616 2.05528L13.7216 4.65691Z" fill="#1D2939"/>
<path d="M1.024 14.9594H2.1248L12.2624 4.65691L11.1616 3.53821L1.024 13.8407V14.9594Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B