mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-11 23:54:43 +08:00
Merge pull request #6532 from aignatov-bio/ai-sci-9588-add-paste-image
Add image paster [SCI-9588]
This commit is contained in:
commit
605d5bfcb7
8 changed files with 143 additions and 37 deletions
|
@ -1,7 +1,7 @@
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
||||||
.sci-label {
|
.sci-label {
|
||||||
@apply text-sm font-medium text-sn-grey;
|
@apply text-sm font-medium text-sn-dark-grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sci-input-container-v2 {
|
.sci-input-container-v2 {
|
||||||
|
|
|
@ -210,6 +210,14 @@
|
||||||
@publish="publishProtocol"
|
@publish="publishProtocol"
|
||||||
@cancel="closePublishModal"
|
@cancel="closePublishModal"
|
||||||
/>
|
/>
|
||||||
|
<clipboardPasteModal v-if="showClipboardPasteModal"
|
||||||
|
:image="pasteImages"
|
||||||
|
:objects="steps"
|
||||||
|
:objectType="'step'"
|
||||||
|
:selectedObjectId="firstObjectInViewport()"
|
||||||
|
@files="uploadFilesToStep"
|
||||||
|
@cancel="showClipboardPasteModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -221,6 +229,8 @@
|
||||||
import Tinymce from '../shared/tinymce.vue'
|
import Tinymce from '../shared/tinymce.vue'
|
||||||
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
||||||
import PublishProtocol from './modals/publish_protocol.vue'
|
import PublishProtocol from './modals/publish_protocol.vue'
|
||||||
|
import clipboardPasteModal from '../shared/content/attachments/clipboard_paste_modal.vue'
|
||||||
|
import AssetPasteMixin from '../shared/content/attachments/mixins/paste.js'
|
||||||
|
|
||||||
import UtilsMixin from '../mixins/utils.js'
|
import UtilsMixin from '../mixins/utils.js'
|
||||||
import stackableHeadersMixin from '../mixins/stackableHeadersMixin';
|
import stackableHeadersMixin from '../mixins/stackableHeadersMixin';
|
||||||
|
@ -234,8 +244,8 @@
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Step, InlineEdit, ProtocolOptions, Tinymce, ReorderableItemsModal, ProtocolMetadata, PublishProtocol},
|
components: { Step, InlineEdit, ProtocolOptions, Tinymce, ReorderableItemsModal, ProtocolMetadata, PublishProtocol, clipboardPasteModal},
|
||||||
mixins: [UtilsMixin, stackableHeadersMixin, moduleNameObserver],
|
mixins: [UtilsMixin, stackableHeadersMixin, moduleNameObserver, AssetPasteMixin],
|
||||||
computed: {
|
computed: {
|
||||||
inRepository() {
|
inRepository() {
|
||||||
return this.protocol.attributes.in_repository
|
return this.protocol.attributes.in_repository
|
||||||
|
@ -245,7 +255,7 @@
|
||||||
},
|
},
|
||||||
urls() {
|
urls() {
|
||||||
return this.protocol.attributes.urls || {}
|
return this.protocol.attributes.urls || {}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -433,6 +443,16 @@
|
||||||
$('.my_module-name .view-mode').trigger('click');
|
$('.my_module-name .view-mode').trigger('click');
|
||||||
$('.my_module-name .input-field').focus();
|
$('.my_module-name .input-field').focus();
|
||||||
}, 300)
|
}, 300)
|
||||||
|
},
|
||||||
|
uploadFilesToStep(file, stepId) {
|
||||||
|
this.$children.find(child => child.step?.id == stepId).uploadFiles(file);
|
||||||
|
},
|
||||||
|
firstObjectInViewport() {
|
||||||
|
let step = $('.step-container:not(.locked)').toArray().find(element => {
|
||||||
|
const { top, bottom } = element.getBoundingClientRect()
|
||||||
|
return bottom > 0 && top < window.innerHeight
|
||||||
|
})
|
||||||
|
return step ? step.dataset.id : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
@drop.prevent="dropFile"
|
@drop.prevent="dropFile"
|
||||||
@dragenter.prevent="dragEnter($event)"
|
@dragenter.prevent="dragEnter($event)"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
:class="{ 'draging-file': dragingFile, 'editing-name': editingName }"
|
:data-id="step.id"
|
||||||
|
:class="{ 'draging-file': dragingFile, 'editing-name': editingName, 'locked': !urls.update_url }"
|
||||||
>
|
>
|
||||||
<div class="drop-message" @dragleave.prevent="!showFileModal ? dragingFile = false : null">
|
<div class="drop-message" @dragleave.prevent="!showFileModal ? dragingFile = false : null">
|
||||||
{{ i18n.t('protocols.steps.drop_message', { position: step.attributes.position + 1 }) }}
|
{{ i18n.t('protocols.steps.drop_message', { position: step.attributes.position + 1 }) }}
|
||||||
|
@ -134,12 +135,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<deleteStepModal v-if="confirmingDelete" @confirm="deleteStep" @cancel="closeDeleteModal"/>
|
<deleteStepModal v-if="confirmingDelete" @confirm="deleteStep" @cancel="closeDeleteModal"/>
|
||||||
<clipboardPasteModal v-if="showClipboardPasteModal"
|
|
||||||
:parent="step"
|
|
||||||
:image="pasteImages"
|
|
||||||
@files="uploadFiles"
|
|
||||||
@cancel="showClipboardPasteModal = false"
|
|
||||||
/>
|
|
||||||
<ReorderableItemsModal v-if="reordering"
|
<ReorderableItemsModal v-if="reordering"
|
||||||
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
|
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
|
||||||
:items="reorderableElements"
|
:items="reorderableElements"
|
||||||
|
@ -162,7 +157,6 @@
|
||||||
import Checklist from '../shared/content/checklist.vue'
|
import Checklist from '../shared/content/checklist.vue'
|
||||||
import deleteStepModal from './modals/delete_step.vue'
|
import deleteStepModal from './modals/delete_step.vue'
|
||||||
import Attachments from '../shared/content/attachments.vue'
|
import Attachments from '../shared/content/attachments.vue'
|
||||||
import clipboardPasteModal from '../shared/content/attachments/clipboard_paste_modal.vue'
|
|
||||||
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
||||||
import MenuDropdown from '../shared/menu_dropdown.vue'
|
import MenuDropdown from '../shared/menu_dropdown.vue'
|
||||||
|
|
||||||
|
@ -202,7 +196,6 @@
|
||||||
attachmentsReady: false,
|
attachmentsReady: false,
|
||||||
confirmingDelete: false,
|
confirmingDelete: false,
|
||||||
showFileModal: false,
|
showFileModal: false,
|
||||||
showClipboardPasteModal: false,
|
|
||||||
showCommentsSidebar: false,
|
showCommentsSidebar: false,
|
||||||
dragingFile: false,
|
dragingFile: false,
|
||||||
reordering: false,
|
reordering: false,
|
||||||
|
@ -226,7 +219,6 @@
|
||||||
StepText,
|
StepText,
|
||||||
Checklist,
|
Checklist,
|
||||||
deleteStepModal,
|
deleteStepModal,
|
||||||
clipboardPasteModal,
|
|
||||||
Attachments,
|
Attachments,
|
||||||
StorageUsage,
|
StorageUsage,
|
||||||
ReorderableItemsModal,
|
ReorderableItemsModal,
|
||||||
|
@ -542,10 +534,6 @@
|
||||||
rect.right <= (window.innerWidth || $(window).width())
|
rect.right <= (window.innerWidth || $(window).width())
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
copyPasteImageModal(pasteImages) {
|
|
||||||
this.pasteImages = pasteImages;
|
|
||||||
this.showClipboardPasteModal = true;
|
|
||||||
},
|
|
||||||
insertElement(element) {
|
insertElement(element) {
|
||||||
let position = element.attributes.position;
|
let position = element.attributes.position;
|
||||||
this.elements = this.elements.map( s => {
|
this.elements = this.elements.map( s => {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
@drop.prevent="dropFile"
|
@drop.prevent="dropFile"
|
||||||
@dragenter.prevent="dragEnter($event)"
|
@dragenter.prevent="dragEnter($event)"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
:class="{ 'bg-sn-super-light-blue': dragingFile, 'bg-white': !dragingFile }"
|
:data-id="result.id"
|
||||||
|
:class="{ 'bg-sn-super-light-blue': dragingFile, 'bg-white': !dragingFile, 'locked': locked }"
|
||||||
>
|
>
|
||||||
<div class="text-xl items-center flex flex-col text-sn-blue h-full justify-center left-0 absolute top-0 w-full"
|
<div class="text-xl items-center flex flex-col text-sn-blue h-full justify-center left-0 absolute top-0 w-full"
|
||||||
v-if="dragingFile"
|
v-if="dragingFile"
|
||||||
|
|
|
@ -32,6 +32,14 @@
|
||||||
@result:drag_enter="dragEnter"
|
@result:drag_enter="dragEnter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<clipboardPasteModal v-if="showClipboardPasteModal"
|
||||||
|
:image="pasteImages"
|
||||||
|
:objects="results"
|
||||||
|
:objectType="'result'"
|
||||||
|
:selectedObjectId="firstObjectInViewport()"
|
||||||
|
@files="uploadFilesToResult"
|
||||||
|
@cancel="showClipboardPasteModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -43,10 +51,13 @@
|
||||||
import stackableHeadersMixin from '../mixins/stackableHeadersMixin';
|
import stackableHeadersMixin from '../mixins/stackableHeadersMixin';
|
||||||
import moduleNameObserver from '../mixins/moduleNameObserver';
|
import moduleNameObserver from '../mixins/moduleNameObserver';
|
||||||
|
|
||||||
|
import clipboardPasteModal from '../shared/content/attachments/clipboard_paste_modal.vue'
|
||||||
|
import AssetPasteMixin from '../shared/content/attachments/mixins/paste.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Results',
|
name: 'Results',
|
||||||
components: { ResultsToolbar, Result },
|
components: { ResultsToolbar, Result, clipboardPasteModal },
|
||||||
mixins: [stackableHeadersMixin, moduleNameObserver],
|
mixins: [stackableHeadersMixin, moduleNameObserver, AssetPasteMixin],
|
||||||
props: {
|
props: {
|
||||||
url: { type: String, required: true },
|
url: { type: String, required: true },
|
||||||
canCreate: { type: String, required: true },
|
canCreate: { type: String, required: true },
|
||||||
|
@ -136,6 +147,16 @@
|
||||||
},
|
},
|
||||||
dragEnter(id) {
|
dragEnter(id) {
|
||||||
this.activeDragResult = id;
|
this.activeDragResult = id;
|
||||||
|
},
|
||||||
|
uploadFilesToResult(file, resultId) {
|
||||||
|
this.$children.find(child => child.result?.id == resultId).uploadFiles(file);
|
||||||
|
},
|
||||||
|
firstObjectInViewport() {
|
||||||
|
let result = $('.result-wrapper:not(.locked)').toArray().find(element => {
|
||||||
|
const { top, bottom } = element.getBoundingClientRect()
|
||||||
|
return bottom > 0 && top < window.innerHeight
|
||||||
|
})
|
||||||
|
return result ? result.dataset.id : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,29 +12,66 @@
|
||||||
<h4 class="modal-title">{{i18n.t('assets.from_clipboard.modal_title')}}</h4>
|
<h4 class="modal-title">{{i18n.t('assets.from_clipboard.modal_title')}}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p><strong>{{i18n.t('assets.from_clipboard.image_preview')}}</strong></p>
|
<label class="sci-label">{{i18n.t('assets.from_clipboard.image_preview')}}</label>
|
||||||
<canvas style="border:1px solid grey;max-width:400px;max-height:300px" id="clipboardPreview" />
|
<canvas class="w-full max-h-80 rounded border border-solid border-sn-light-grey" ref="preview" />
|
||||||
<p><strong>{{i18n.t('assets.from_clipboard.file_name')}}</strong></p>
|
<div class="w-full py-6">
|
||||||
<div class="input-group">
|
<label class="sci-label">{{i18n.t('assets.from_clipboard.select_target')}}</label>
|
||||||
<input id="clipboardImageName" type="text" class="form-control"
|
<SelectSearch
|
||||||
|
:value="target"
|
||||||
|
@change="setTarget"
|
||||||
|
:options="targets"
|
||||||
|
:isLoading="false"
|
||||||
|
:placeholder="
|
||||||
|
i18n.t(`protocols.steps.modals.move_element.${objectType}.search_placeholder`)
|
||||||
|
"
|
||||||
|
:searchPlaceholder="
|
||||||
|
i18n.t(`protocols.steps.modals.move_element.${objectType}.search_placeholder`)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="sci-label">{{i18n.t('assets.from_clipboard.file_name')}}</label>
|
||||||
|
<div class="sci-input-container-v2">
|
||||||
|
<input id="clipboardImageName" type="text" class="sci-input-field !pr-16" v-model="fileName"
|
||||||
:placeholder="i18n.t('assets.from_clipboard.file_name_placeholder')" aria-describedby="image-name">
|
:placeholder="i18n.t('assets.from_clipboard.file_name_placeholder')" aria-describedby="image-name">
|
||||||
<span class="input-group-addon" id="image-name"></span>
|
<span class="absolute right-2.5 text-sn-grey ">
|
||||||
|
.{{ extension }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" @click="cancel">{{i18n.t('general.cancel')}}</button>
|
<button type="button" class="btn btn-secondary" @click="cancel">{{i18n.t('general.cancel')}}</button>
|
||||||
<button type="button" class="btn btn-success" @click="uploadImage">{{i18n.t('assets.from_clipboard.add_image')}}</button>
|
<button type="button" class="btn btn-success" :disabled="!valid" @click="uploadImage">{{i18n.t('assets.from_clipboard.add_image')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import SelectSearch from "../../select_search.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'clipboardPasteModal',
|
name: 'clipboardPasteModal',
|
||||||
props: {
|
props: {
|
||||||
parent: Object,
|
objects: Array,
|
||||||
image: DataTransferItem
|
image: DataTransferItem,
|
||||||
|
objectType: String,
|
||||||
|
selectedObjectId: String
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
target: null,
|
||||||
|
targets: [],
|
||||||
|
fileName: '',
|
||||||
|
extension: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SelectSearch
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
valid() {
|
||||||
|
return this.target && this.fileName.length > 0;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
$(this.$refs.modal).modal('show');
|
$(this.$refs.modal).modal('show');
|
||||||
|
@ -42,15 +79,26 @@
|
||||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.selectedObjectId) this.target = this.selectedObjectId;
|
||||||
|
this.targets = this.objects.map((object) => {
|
||||||
|
return [
|
||||||
|
object.id,
|
||||||
|
object.attributes.name
|
||||||
|
]
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTarget(target) {
|
||||||
|
this.target = target;
|
||||||
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
$(this.$refs.modal).modal('hide');
|
$(this.$refs.modal).modal('hide');
|
||||||
},
|
},
|
||||||
appendImage(item) {
|
appendImage(item) {
|
||||||
let imageBlob = item.getAsFile();
|
let imageBlob = item.getAsFile();
|
||||||
if (imageBlob) {
|
if (imageBlob) {
|
||||||
var canvas = document.getElementById('clipboardPreview');
|
var canvas = this.$refs.preview;
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
var img = new Image();
|
var img = new Image();
|
||||||
img.onload = function() {
|
img.onload = function() {
|
||||||
|
@ -63,12 +111,12 @@
|
||||||
let extension = imageBlob.name.slice(
|
let extension = imageBlob.name.slice(
|
||||||
(Math.max(0, imageBlob.name.lastIndexOf('.')) || Infinity) + 1
|
(Math.max(0, imageBlob.name.lastIndexOf('.')) || Infinity) + 1
|
||||||
);
|
);
|
||||||
$('#image-name').html('.' + extension); // add extension near file name
|
this.extension = extension;
|
||||||
this.imageBlob = imageBlob;
|
this.imageBlob = imageBlob;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
uploadImage() {
|
uploadImage() {
|
||||||
let newName = $('#clipboardImageName').val();
|
let newName = this.fileName;
|
||||||
let imageBlog = this.imageBlob;
|
let imageBlog = this.imageBlob;
|
||||||
// check if the name is set
|
// check if the name is set
|
||||||
if (newName && newName.length > 0) {
|
if (newName && newName.length > 0) {
|
||||||
|
@ -82,7 +130,7 @@
|
||||||
this.imageBlob = new File([blob], name, { type: imageBlog.type });
|
this.imageBlob = new File([blob], name, { type: imageBlog.type });
|
||||||
}
|
}
|
||||||
$(this.$refs.modal).modal('hide');
|
$(this.$refs.modal).modal('hide');
|
||||||
this.$emit('files', this.imageBlob);
|
this.$emit('files', this.imageBlob, this.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showClipboardPasteModal: false,
|
||||||
|
pasteImages: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('paste', this.handlePaste);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
document.removeEventListener('paste', this.handlePaste);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePaste(e) {
|
||||||
|
if ( e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' ) return;
|
||||||
|
this.pasteImages = this.getImagesFromClipboard(e);
|
||||||
|
if (this.pasteImages && this.firstObjectInViewport()) this.showClipboardPasteModal = true;
|
||||||
|
},
|
||||||
|
getImagesFromClipboard(e) {
|
||||||
|
let image = null;
|
||||||
|
if (e.clipboardData && e.clipboardData.items) image = e.clipboardData.items[0];
|
||||||
|
if (image && image.type.indexOf('image') === -1) image = null
|
||||||
|
return image;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -3454,9 +3454,10 @@ en:
|
||||||
drop_label: 'Drop to add to Step'
|
drop_label: 'Drop to add to Step'
|
||||||
file_label: 'File'
|
file_label: 'File'
|
||||||
from_clipboard:
|
from_clipboard:
|
||||||
modal_title: 'Add image from clipboard'
|
modal_title: 'Insert image from clipboard'
|
||||||
image_preview: 'Image preview'
|
image_preview: 'Pasted image preview'
|
||||||
add_image: 'Add'
|
select_target: 'Select to which step you’d like to attach image'
|
||||||
|
add_image: 'Insert'
|
||||||
file_name: 'File name'
|
file_name: 'File name'
|
||||||
file_name_placeholder: 'Image'
|
file_name_placeholder: 'Image'
|
||||||
placeholder:
|
placeholder:
|
||||||
|
|
Loading…
Add table
Reference in a new issue