mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-07 20:40:26 +08:00
Add flyout menu to step/results elements [SCI-9262]
This commit is contained in:
parent
7bb5305b14
commit
3bee8feb7d
10 changed files with 196 additions and 51 deletions
|
@ -4,9 +4,12 @@ import TurbolinksAdapter from 'vue-turbolinks';
|
||||||
import Vue from 'vue/dist/vue.esm';
|
import Vue from 'vue/dist/vue.esm';
|
||||||
import ProtocolContainer from '../../vue/protocol/container.vue';
|
import ProtocolContainer from '../../vue/protocol/container.vue';
|
||||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||||
|
import outsideClick from './directives/outside_click';
|
||||||
|
|
||||||
|
|
||||||
Vue.use(PerfectScrollbar);
|
Vue.use(PerfectScrollbar);
|
||||||
Vue.use(TurbolinksAdapter);
|
Vue.use(TurbolinksAdapter);
|
||||||
|
Vue.directive('click-outside', outsideClick);
|
||||||
Vue.prototype.i18n = window.I18n;
|
Vue.prototype.i18n = window.I18n;
|
||||||
Vue.prototype.inlineEditing = window.inlineEditing;
|
Vue.prototype.inlineEditing = window.inlineEditing;
|
||||||
Vue.prototype.ActiveStoragePreviews = window.ActiveStoragePreviews;
|
Vue.prototype.ActiveStoragePreviews = window.ActiveStoragePreviews;
|
||||||
|
|
|
@ -2,9 +2,12 @@ import TurbolinksAdapter from 'vue-turbolinks';
|
||||||
import Vue from 'vue/dist/vue.esm';
|
import Vue from 'vue/dist/vue.esm';
|
||||||
import Results from '../../vue/results/results.vue';
|
import Results from '../../vue/results/results.vue';
|
||||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||||
|
import outsideClick from './directives/outside_click';
|
||||||
|
|
||||||
|
|
||||||
Vue.use(PerfectScrollbar);
|
Vue.use(PerfectScrollbar);
|
||||||
Vue.use(TurbolinksAdapter);
|
Vue.use(TurbolinksAdapter);
|
||||||
|
Vue.directive('click-outside', outsideClick);
|
||||||
Vue.prototype.i18n = window.I18n;
|
Vue.prototype.i18n = window.I18n;
|
||||||
Vue.prototype.ActiveStoragePreviews = window.ActiveStoragePreviews;
|
Vue.prototype.ActiveStoragePreviews = window.ActiveStoragePreviews;
|
||||||
|
|
||||||
|
|
|
@ -22,20 +22,17 @@
|
||||||
@update="updateName"
|
@update="updateName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden group-hover/checklist-header:flex items-center gap-2 ml-auto">
|
<MenuDropdown
|
||||||
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light btn-sm" @click="editingName = true" tabindex="0">
|
class="ml-auto"
|
||||||
<i class="sn-icon sn-icon-edit"></i>
|
:listItems="this.actionMenu"
|
||||||
</button>
|
:btnClasses="'btn btn-light icon-btn'"
|
||||||
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light btn-sm" tabindex="0" @click="duplicateElement">
|
:position="'right'"
|
||||||
<i class="sn-icon sn-icon-duplicate"></i>
|
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||||
</button>
|
@edit="editingName = true"
|
||||||
<button v-if="element.attributes.orderable.urls.move_targets_url" class="btn btn-light btn-sm" tabindex="0" @click="showMoveModal">
|
@duplicate="duplicateElement"
|
||||||
Move
|
@move="showMoveModal"
|
||||||
</button>
|
@delete="showDeleteModal"
|
||||||
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light btn-sm" @click="showDeleteModal" tabindex="0">
|
></MenuDropdown>
|
||||||
<i class="sn-icon sn-icon-delete"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="element.attributes.orderable.urls.create_item_url || orderedChecklistItems.length > 0">
|
<div v-if="element.attributes.orderable.urls.create_item_url || orderedChecklistItems.length > 0">
|
||||||
<Draggable
|
<Draggable
|
||||||
|
@ -95,10 +92,11 @@
|
||||||
import ChecklistItem from './checklistItem.vue'
|
import ChecklistItem from './checklistItem.vue'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import moveElementModal from './modal/move.vue'
|
import moveElementModal from './modal/move.vue'
|
||||||
|
import MenuDropdown from '../menu_dropdown.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Checklist',
|
name: 'Checklist',
|
||||||
components: { deleteElementModal, InlineEdit, ChecklistItem, Draggable, moveElementModal },
|
components: { deleteElementModal, InlineEdit, ChecklistItem, Draggable, moveElementModal, MenuDropdown },
|
||||||
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
@ -153,6 +151,34 @@
|
||||||
},
|
},
|
||||||
locked() {
|
locked() {
|
||||||
return this.reordering || this.editingName || !this.element.attributes.orderable.urls.update_url
|
return this.reordering || this.editingName || !this.element.attributes.orderable.urls.update_url
|
||||||
|
},
|
||||||
|
actionMenu() {
|
||||||
|
let menu = [];
|
||||||
|
if (this.element.attributes.orderable.urls.update_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.edit'),
|
||||||
|
emit: 'edit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.duplicate_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.duplicate'),
|
||||||
|
emit: 'duplicate'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.move_targets_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.move'),
|
||||||
|
emit: 'move'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.delete_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.delete'),
|
||||||
|
emit: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-6"></div>
|
<div v-else class="w-6"></div>
|
||||||
<div class="whitespace-nowrap text-ellipsis overflow-hidden grow" :class="{ 'pointer-events-none': !checklistItem.attributes.isNew && !updateUrl }">
|
<div class="grow" :class="{ 'pointer-events-none': !checklistItem.attributes.isNew && !updateUrl }">
|
||||||
<InlineEdit
|
<InlineEdit
|
||||||
:value="checklistItem.attributes.text"
|
:value="checklistItem.attributes.text"
|
||||||
:sa_value="checklistItem.attributes.sa_text"
|
:sa_value="checklistItem.attributes.sa_text"
|
||||||
|
|
|
@ -5,8 +5,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showDeleteModal(event) {
|
showDeleteModal() {
|
||||||
event.stopPropagation();
|
|
||||||
this.confirmingDelete = true;
|
this.confirmingDelete = true;
|
||||||
},
|
},
|
||||||
closeDeleteModal() {
|
closeDeleteModal() {
|
||||||
|
|
|
@ -7,8 +7,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showMoveModal(event) {
|
showMoveModal() {
|
||||||
event.stopPropagation();
|
|
||||||
this.movingElement = true;
|
this.movingElement = true;
|
||||||
},
|
},
|
||||||
closeMoveModal() {
|
closeMoveModal() {
|
||||||
|
|
|
@ -23,26 +23,23 @@
|
||||||
@update="updateName"
|
@update="updateName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden group-hover/table-header:flex items-center gap-2 ml-auto">
|
<MenuDropdown
|
||||||
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="enableNameEdit" tabindex="0">
|
class="ml-auto"
|
||||||
<i class="sn-icon sn-icon-edit"></i>
|
:listItems="this.actionMenu"
|
||||||
</button>
|
:btnClasses="'btn btn-light icon-btn'"
|
||||||
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="0" @click="duplicateElement">
|
:position="'right'"
|
||||||
<i class="sn-icon sn-icon-duplicate"></i>
|
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||||
</button>
|
@edit="enableNameEdit"
|
||||||
<button v-if="element.attributes.orderable.urls.move_targets_url" class="btn btn-light btn-sm" tabindex="0" @click="showMoveModal">
|
@duplicate="duplicateElement"
|
||||||
Move
|
@move="showMoveModal"
|
||||||
</button>
|
@delete="showDeleteModal"
|
||||||
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="0">
|
></MenuDropdown>
|
||||||
<i class="sn-icon sn-icon-delete"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-body ml-8 group/table-body relative p-1 border-solid border-transparent"
|
<div class="table-body ml-8 group/table-body relative p-1 border-solid border-transparent"
|
||||||
:class="{'edit border-sn-light-grey': editingTable, 'view': !editingTable, 'locked': !element.attributes.orderable.urls.update_url}"
|
:class="{'edit border-sn-light-grey': editingTable, 'view': !editingTable, 'locked': !element.attributes.orderable.urls.update_url}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.enter="!editingTable && enableTableEdit()">
|
@keyup.enter="!editingTable && enableTableEdit()">
|
||||||
<div class="absolute right-0 top-0 z-[200]" v-if="!editingTable && element.attributes.orderable.urls.update_url" @click="enableTableEdit">
|
<div class="absolute right-0 top-0 z-40" v-if="!editingTable && element.attributes.orderable.urls.update_url" @click="enableTableEdit">
|
||||||
<div class="bg-sn-light-grey rounded tw-hidden group-hover/table-body:flex cursor-pointer p-1">
|
<div class="bg-sn-light-grey rounded tw-hidden group-hover/table-body:flex cursor-pointer p-1">
|
||||||
|
|
||||||
<i class="sn-icon sn-icon-edit"></i>
|
<i class="sn-icon sn-icon-edit"></i>
|
||||||
|
@ -79,10 +76,11 @@
|
||||||
import InlineEdit from '../inline_edit.vue'
|
import InlineEdit from '../inline_edit.vue'
|
||||||
import TableNameModal from './modal/table_name.vue'
|
import TableNameModal from './modal/table_name.vue'
|
||||||
import moveElementModal from './modal/move.vue'
|
import moveElementModal from './modal/move.vue'
|
||||||
|
import MenuDropdown from '../menu_dropdown.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ContentTable',
|
name: 'ContentTable',
|
||||||
components: { deleteElementModal, InlineEdit, TableNameModal, moveElementModal },
|
components: { deleteElementModal, InlineEdit, TableNameModal, moveElementModal, MenuDropdown },
|
||||||
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
@ -117,6 +115,34 @@
|
||||||
computed: {
|
computed: {
|
||||||
locked() {
|
locked() {
|
||||||
return !this.element.attributes.orderable.urls.update_url
|
return !this.element.attributes.orderable.urls.update_url
|
||||||
|
},
|
||||||
|
actionMenu() {
|
||||||
|
let menu = [];
|
||||||
|
if (this.element.attributes.orderable.urls.update_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.edit'),
|
||||||
|
emit: 'edit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.duplicate_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.duplicate'),
|
||||||
|
emit: 'duplicate'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.move_targets_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.move'),
|
||||||
|
emit: 'move'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.delete_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.delete'),
|
||||||
|
emit: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
|
|
|
@ -24,20 +24,17 @@
|
||||||
@update="updateName"
|
@update="updateName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden group-hover/text-header:flex items-center gap-2 ml-auto" >
|
<MenuDropdown
|
||||||
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="enableNameEdit" tabindex="0">
|
class="ml-auto"
|
||||||
<i class="sn-icon sn-icon-edit"></i>
|
:listItems="this.actionMenu"
|
||||||
</button>
|
:btnClasses="'btn btn-light icon-btn'"
|
||||||
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="0" @click="duplicateElement">
|
:position="'right'"
|
||||||
<i class="sn-icon sn-icon-duplicate"></i>
|
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||||
</button>
|
@edit="enableNameEdit"
|
||||||
<button v-if="element.attributes.orderable.urls.move_targets_url" class="btn btn-light btn-sm" tabindex="0" @click="showMoveModal">
|
@duplicate="duplicateElement"
|
||||||
Move
|
@move="showMoveModal"
|
||||||
</button>
|
@delete="showDeleteModal"
|
||||||
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="0">
|
></MenuDropdown>
|
||||||
<i class="sn-icon sn-icon-delete"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex rounded ml-8 pl-1 min-h-[2.25rem] mb-4 relative w-full group/text_container content__text-body" :class="{ 'edit': inEditMode, 'component__element--locked': !element.attributes.orderable.urls.update_url }" @keyup.enter="enableEditMode($event)" tabindex="0">
|
<div class="flex rounded ml-8 pl-1 min-h-[2.25rem] mb-4 relative w-full group/text_container content__text-body" :class="{ 'edit': inEditMode, 'component__element--locked': !element.attributes.orderable.urls.update_url }" @keyup.enter="enableEditMode($event)" tabindex="0">
|
||||||
<Tinymce
|
<Tinymce
|
||||||
|
@ -78,10 +75,11 @@
|
||||||
import moveElementModal from './modal/move.vue'
|
import moveElementModal from './modal/move.vue'
|
||||||
import InlineEdit from '../inline_edit.vue'
|
import InlineEdit from '../inline_edit.vue'
|
||||||
import Tinymce from '../tinymce.vue'
|
import Tinymce from '../tinymce.vue'
|
||||||
|
import MenuDropdown from '../menu_dropdown.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextContent',
|
name: 'TextContent',
|
||||||
components: { deleteElementModal, Tinymce, moveElementModal, InlineEdit },
|
components: { deleteElementModal, Tinymce, moveElementModal, InlineEdit, MenuDropdown },
|
||||||
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
mixins: [DeleteMixin, DuplicateMixin, MoveMixin],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
@ -115,6 +113,36 @@
|
||||||
this.enableEditMode();
|
this.enableEditMode();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
actionMenu() {
|
||||||
|
let menu = [];
|
||||||
|
if (this.element.attributes.orderable.urls.update_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.edit'),
|
||||||
|
emit: 'edit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.duplicate_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.duplicate'),
|
||||||
|
emit: 'duplicate'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.move_targets_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.move'),
|
||||||
|
emit: 'move'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.delete_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.delete'),
|
||||||
|
emit: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
enableEditMode() {
|
enableEditMode() {
|
||||||
if (!this.element.attributes.orderable.urls.update_url) return
|
if (!this.element.attributes.orderable.urls.update_url) return
|
||||||
|
|
60
app/javascript/vue/shared/menu_dropdown.vue
Normal file
60
app/javascript/vue/shared/menu_dropdown.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative" v-if="listItems.length > 0" >
|
||||||
|
<button ref="openBtn" :class="btnClasses" @click="showMenu = !showMenu">
|
||||||
|
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||||
|
{{ btnText }}
|
||||||
|
<i v-if="caret && showMenu" class="fas fa-caret-up"></i>
|
||||||
|
<i v-else-if="caret" class="fas fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<div ref="flyout"
|
||||||
|
class="absolute z-50 bg-sn-white rounded p-2.5 sn-shadow-menu-sm"
|
||||||
|
:class="{'right-0': position === 'right', 'left-0': position === 'left'}"
|
||||||
|
v-if="showMenu"
|
||||||
|
v-click-outside="{handler: 'closeMenu', exclude: ['openBtn', 'flyout']}">
|
||||||
|
<a v-for="(item, i) in listItems"
|
||||||
|
:key="i"
|
||||||
|
:href="item.url"
|
||||||
|
class="block whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer hover:bg-sn-super-light-blue"
|
||||||
|
@click="handleClick($event, item)"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DropdownMenu',
|
||||||
|
props: {
|
||||||
|
listItems: { type: Array, required: true },
|
||||||
|
position: { type: String, default: 'left' },
|
||||||
|
btnClasses: { type: String, default: 'btn btn-light' },
|
||||||
|
btnText: { type: String, required: false },
|
||||||
|
btnIcon: { type: String, required: false },
|
||||||
|
caret: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMenu: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeMenu() {
|
||||||
|
this.showMenu = false;
|
||||||
|
},
|
||||||
|
handleClick(event, item) {
|
||||||
|
if (!item.url) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.emit) {
|
||||||
|
this.$emit(item.emit)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showMenu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -3587,6 +3587,7 @@ en:
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
|
duplicate: "Duplicate"
|
||||||
okay: "Okay"
|
okay: "Okay"
|
||||||
back: "Back"
|
back: "Back"
|
||||||
close: "Close"
|
close: "Close"
|
||||||
|
|
Loading…
Add table
Reference in a new issue