Merge pull request #6123 from ivanscinote/SCI-9016-inline-edit-component-ui-rework

Rework inline edit vue component UI [SCI-9016]
This commit is contained in:
aignatov-bio 2023-09-06 10:49:18 +02:00 committed by GitHub
commit b5883178a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 155 additions and 174 deletions

View file

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

View file

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

View file

@ -1,95 +1,11 @@
// scss-lint:disable SelectorDepth // scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth // 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-cursor-edit {
.sci-inline-edit__content { cursor: url("/images/icon_small/edit.svg") 0 16, auto;
padding-top: 0; }
}
} .inline-edit-placeholder:empty::before {
content: attr(placeholder);
.sci-inline-edit__content { display: block;
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;
}
}
}
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -44,6 +44,7 @@
:allowBlank="false" :allowBlank="false"
:attributeName="`${i18n.t('Step')} ${i18n.t('name')}`" :attributeName="`${i18n.t('Step')} ${i18n.t('name')}`"
:autofocus="editingName" :autofocus="editingName"
:timestamp="i18n.t('protocols.steps.timestamp', { date: step.attributes.created_at, user: step.attributes.created_by })"
:placeholder="i18n.t('protocols.steps.placeholder')" :placeholder="i18n.t('protocols.steps.placeholder')"
:defaultValue="i18n.t('protocols.steps.default_name')" :defaultValue="i18n.t('protocols.steps.default_name')"
@editingEnabled="editingName = true" @editingEnabled="editingName = true"
@ -52,9 +53,6 @@
@update="updateName" @update="updateName"
/> />
</div> </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>
<div class="elements-actions-container"> <div class="elements-actions-container">
<input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple /> <input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple />
@ -158,7 +156,6 @@
</div> </div>
<div class="collapse in" :id="'stepBody' + step.id"> <div class="collapse in" :id="'stepBody' + step.id">
<div class="step-elements"> <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"> <template v-for="(element, index) in orderedElements">
<component <component
:is="elements[index].attributes.orderable_type" :is="elements[index].attributes.orderable_type"
@ -261,6 +258,7 @@
reordering: false, reordering: false,
isCollapsed: false, isCollapsed: false,
editingName: false, editingName: false,
inlineEditError: null,
wellPlateOptions: [ wellPlateOptions: [
{ label: 'protocols.steps.insert.well_plate_options.32_x_48', dimensions: [32, 48] }, { 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] }, { label: 'protocols.steps.insert.well_plate_options.16_x_24', dimensions: [16, 24] },

View file

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

View file

@ -1,32 +1,41 @@
<template> <template>
<div class="sci-inline-edit" :class="{ 'editing': editing }" tabindex="0" @keyup.enter="enableEdit($event)"> <div class="flex flex-col">
<div class="sci-inline-edit__content" :class="{ 'error': error }"> <span
<textarea v-if="editing"
ref="input" ref="input"
rows="1" contenteditable="true"
v-if="editing" class="outline-none p-0 pb-2 border-0 border-solid border-b w-fit"
:placeholder="placeholder" :class="{ 'inline-edit-placeholder text-sn-grey caret-black': isBlank, 'border-sn-delete-red': error, 'border-sn-science-blue': !error }"
v-model="newValue" :placeholder="placeholder"
@input="handleInput" @input="handleInput"
@keydown="handleKeypress" @keydown="handleKeypress"
@paste="handlePaste" @paste="handlePaste"
@blur="handleBlur" @blur="handleBlur"
@keyup.escape="cancelEdit" @keyup.escape="cancelEdit"
></textarea> @focus="setCaretAtEnd"
<div v-else-if="smartAnnotation" @click="enableEdit($event)" class="sci-inline-edit__view" v-html="sa_value || placeholder" :class="{ 'blank': isBlank }"></div> ></span>
<div v-else @click="enableEdit($event)" class="sci-inline-edit__view" :class="[isBlank ? 'blank' : '', ...customClasses]">{{newValue || placeholder}}</div> <div
<div v-if="editing && error" class="sci-inline-edit__error"> v-else-if="smartAnnotation"
{{ error }} class="sci-cursor-edit"
</div> :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> </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> </div>
</template> </template>
@ -42,6 +51,7 @@
attributeName: { type: String, required: true }, attributeName: { type: String, required: true },
characterLimit: { type: Number }, characterLimit: { type: Number },
characterMinLimit: { type: Number }, characterMinLimit: { type: Number },
timestamp: { type: String},
placeholder: { type: String }, placeholder: { type: String },
autofocus: { type: Boolean, default: false }, autofocus: { type: Boolean, default: false },
saveOnEnter: { type: Boolean, default: true }, saveOnEnter: { type: Boolean, default: true },
@ -49,8 +59,7 @@
multilinePaste: { type: Boolean, default: false }, multilinePaste: { type: Boolean, default: false },
smartAnnotation: { type: Boolean, default: false }, smartAnnotation: { type: Boolean, default: false },
editOnload: { type: Boolean, default: false }, editOnload: { type: Boolean, default: false },
defaultValue: { type: String, default: '' }, defaultValue: { type: String, default: '' }
customClasses: { type: Array, default: () => [] }
}, },
data() { data() {
return { return {
@ -60,7 +69,7 @@
} }
}, },
mixins: [UtilsMixin], mixins: [UtilsMixin],
created( ){ created() {
this.newValue = this.value || ''; this.newValue = this.value || '';
}, },
mounted() { mounted() {
@ -70,16 +79,22 @@
} }
}, },
watch: { watch: {
newValue() {
if (this.newValue.length === 0 && this.editing) {
this.focus();
this.setCaretPosition();
}
},
autofocus() { autofocus() {
this.handleAutofocus(); this.handleAutofocus();
},
value() {
this.newValue = this.value;
} }
}, },
computed: { computed: {
isBlank() { isBlank() {
return this.newValue.length === 0 return this.newValue.length === 0;
},
isContentDefault() {
return this.newValue === this.defaultValue;
}, },
error() { error() {
if(!this.allowBlank && this.isBlank) { if(!this.allowBlank && this.isBlank) {
@ -118,7 +133,6 @@
}, },
handleBlur() { handleBlur() {
if ($('.atwho-view:visible').length) return; if ($('.atwho-view:visible').length) return;
if (this.allowBlank || !this.isBlank) { if (this.allowBlank || !this.isBlank) {
this.$nextTick(this.update); this.$nextTick(this.update);
} else { } else {
@ -128,21 +142,47 @@
focus() { focus() {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.input) return; if (!this.$refs.input) return;
this.$refs.input.focus(); 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) { enableEdit(e) {
if (e && $(e.target).hasClass('atwho-user-popover')) return; if (e && $(e.target).hasClass('atwho-user-popover')) return;
if (e && $(e.target).hasClass('sa-link')) return; if (e && $(e.target).hasClass('sa-link')) return;
if (e && $(e.target).parent().hasClass('atwho-inserted')) return; if (e && $(e.target).parent().hasClass('atwho-inserted')) return;
this.editing = true; this.editing = true;
this.focus();
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.input.value === this.defaultValue) { this.focus();
this.$refs.input.select(); 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) { if (this.smartAnnotation) {
SmartAnnotation.init($(this.$refs.input), false); SmartAnnotation.init($(this.$refs.input), false);
@ -156,40 +196,57 @@
this.$emit('editingDisabled'); this.$emit('editingDisabled');
}, },
handlePaste(e) { handlePaste(e) {
e.preventDefault();
this.dirty = true; this.dirty = true;
if (!this.multilinePaste) return; const clipboardData = (e.clipboardData || window.clipboardData).getData("text");
let lines = (e.originalEvent || e).clipboardData.getData('text/plain').split(/[\n\r]/); let lines = clipboardData.split(/[\n\r]/).filter((l) => l).map((l) => l.trim());
lines = lines.filter((l) => l).map((l) => l.trim());
const selection = window.getSelection();
if (!selection.rangeCount) return;
if (lines.length > 1) { selection.deleteFromDocument();
this.newValue = lines[0];
this.$emit('multilinePaste', lines); let textNode = document.createTextNode(lines[0]);
this.update(); 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; this.dirty = true;
if (!this.allowNewLine) {
this.newValue = this.newValue.replace(/^[\n\r]+|[\n\r]+$/g, ''); const sel = document.getSelection();
} const offset = sel.anchorOffset;
this.$nextTick(this.resize);
this.newValue = this.allowNewLine ? e.target.textContent : e.target.textContent.replace(/^[\n\r]+|[\n\r]+$/g, '');
sel.collapse(sel.anchorNode, offset);
}, },
handleKeypress(e) { handleKeypress(e) {
if (e.key == 'Escape') { if (e.key == 'Escape') {
this.cancelEdit(); this.cancelEdit();
} else if (e.key == 'Enter' && this.saveOnEnter) { } else if (e.key == 'Enter' && this.saveOnEnter) {
e.preventDefault()
this.update(); this.update();
} else { } else {
if (!this.error) this.$emit('error-cleared');
this.dirty = true; 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() { update() {
if (!this.dirty && !this.isBlank) { if (!this.dirty && !this.isBlank) {
this.editing = false; this.editing = false;
@ -198,8 +255,8 @@
if(this.error) return; if(this.error) return;
if(!this.$refs.input) 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.editing = false;
this.$emit('editingDisabled'); this.$emit('editingDisabled');
this.$emit('update', this.newValue); this.$emit('update', this.newValue);

View file

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