Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/js/components/blueprints/Tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class="h-4 w-4 me-1"
/>

{{ __(tab.display) }}
<span class="block max-w-48 overflow-clip text-ellipsis whitespace-nowrap" v-tooltip="__(tab.display).length > 24 ? __(tab.display) : null">{{ __(tab.display) }}</span>

<Dropdown v-if="isActive" placement="left-start" class="me-3">
<template #trigger>
Expand Down
121 changes: 105 additions & 16 deletions resources/js/components/blueprints/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,57 @@
<div>
<Tabs v-model="currentTab" :unmount-on-hide="false">
<div v-if="!singleTab && tabs.length > 0" class="flex items-center justify-between gap-x-2 mb-6">
<TabList class="flex-1">
<div ref="tabs" class="flex-1 flex items-center gap-x-2.5">
<BlueprintTab
ref="tab"
v-for="tab in tabs"
:key="tab._id"
:tab="tab"
:current-tab="currentTab"
:show-instructions="showTabInstructionsField"
:edit-text="editTabText"
@removed="removeTab(tab._id)"
@updated="updateTab(tab._id, $event)"
@mouseenter="mouseEnteredTab(tab._id)"
/>
<TabList class="flex-1 min-w-0 overflow-x-clip overflow-y-visible pe-0.25">
<div ref="tabs" class="flex-1 flex items-center gap-x-2.5 min-w-0">
<div ref="tabWrapper" class="min-w-0 flex-1 flex overflow-clip px-0.25">
<div ref="tabInner" class="flex items-center gap-x-2.5 shrink-0">
<BlueprintTab
ref="tab"
v-for="tab in tabs"
:key="tab._id"
:tab="tab"
:current-tab="currentTab"
:show-instructions="showTabInstructionsField"
:edit-text="editTabText"
@removed="removeTab(tab._id)"
@updated="updateTab(tab._id, $event)"
@mouseenter="mouseEnteredTab(tab._id)"
/>
</div>
</div>
<Dropdown
v-if="overflowedTabs.length"
align="end"
side="bottom"
class="shrink-0"
>
<template #trigger>
<Button
icon="dots"
variant="ghost"
size="sm"
:aria-label="__('Open dropdown menu')"
/>
</template>
<DropdownMenu>
<DropdownItem
v-for="tab in overflowedTabs"
:key="tab._id"
:icon="tab.icon"
:class="{ 'bg-gray-100 dark:bg-gray-800': currentTab === tab._id }"
@click="selectTab(tab._id)"
>
<span class="block max-w-48 overflow-hidden text-ellipsis whitespace-nowrap">
{{ __(tab.display) }}
</span>
</DropdownItem>
<template v-if="activeTabIsOverflowed">
<DropdownSeparator />
<DropdownItem :text="__('Edit')" icon="edit" @click="editActiveOverflowedTab" />
<DropdownItem :text="__('Delete')" icon="trash" variant="destructive" @click="removeActiveOverflowedTab" />
</template>
</DropdownMenu>
</Dropdown>
</div>
</TabList>

Expand Down Expand Up @@ -54,10 +91,11 @@
<script>
import { Sortable, Plugins } from '@shopify/draggable';
import { nanoid as uniqid } from 'nanoid';
import { createTabsOverflowTracker } from '@/util/tabs-overflow.js';
import BlueprintTab from './Tab.vue';
import BlueprintTabContent from './TabContent.vue';
import CanDefineLocalizable from '../fields/CanDefineLocalizable';
import { Tabs, TabList, Button, Description } from '@/components/ui';
import { Tabs, TabList, Button, Description, Dropdown, DropdownMenu, DropdownItem, DropdownSeparator } from '@/components/ui';

export default {
mixins: [CanDefineLocalizable],
Expand All @@ -69,6 +107,10 @@ export default {
TabList,
Button,
Description,
Dropdown,
DropdownMenu,
DropdownItem,
DropdownSeparator,
},

props: {
Expand Down Expand Up @@ -138,31 +180,59 @@ export default {
sortableTabs: null,
sortableSections: null,
sortableFields: null,
overflowedTabs: [],
};
},

computed: {
activeTabIsOverflowed() {
return this.overflowedTabs.some((t) => t._id === this.currentTab);
},
},

watch: {
currentTab() {
this.$nextTick(this.checkOverflow);
},
tabs: {
deep: true,
handler(tabs) {
this.$emit('updated', tabs);
this.makeSortable();
this.$nextTick(this.checkOverflow);
},
},
},

mounted() {
this.ensureTab();
this.makeSortable();
this.overflowTracker = createTabsOverflowTracker({
getWrapper: () => this.$refs.tabWrapper,
getInner: () => this.$refs.tabInner,
getItems: () => this.tabs,
onUpdate: ({ overflowedItems }) => {
this.overflowedTabs = overflowedItems;
},
});
this.$nextTick(() => {
this.overflowTracker.observe();
this.overflowTracker.checkOverflow();
});
},

unmounted() {
if (this.sortableTabs) this.sortableTabs.destroy();
if (this.sortableSections) this.sortableSections.destroy();
if (this.sortableFields) this.sortableFields.destroy();
this.overflowTracker?.disconnect();
},

methods: {
checkOverflow() {
this.overflowTracker?.checkOverflow();
},

ensureTab() {
if (this.requireSection && this.tabs.length === 0) {
this.addTab();
Expand All @@ -180,7 +250,10 @@ export default {
makeTabsSortable() {
if (this.sortableTabs) this.sortableTabs.destroy();

this.sortableTabs = new Sortable(this.$refs.tabs, {
const container = this.$refs.tabInner || this.$refs.tabs;
if (!container) return;

this.sortableTabs = new Sortable(container, {
draggable: '.blueprint-tab',
mirror: { constrainDimensions: true },
swapAnimation: { horizontal: true },
Expand Down Expand Up @@ -294,6 +367,22 @@ export default {
this.currentTab = tabId;
},

editOverflowedTab(tab) {
if (!tab) return;
const refs = this.$refs.tab;
const tabRef = Array.isArray(refs) ? refs.find((c) => c.tab?._id === tab._id) : refs;
tabRef?.edit();
},

editActiveOverflowedTab() {
const tab = this.overflowedTabs.find((t) => t._id === this.currentTab);
if (tab) this.editOverflowedTab(tab);
},

removeActiveOverflowedTab() {
this.removeTab(this.currentTab);
},

mouseEnteredTab(tabId) {
if (this.lastInteractedTab) this.selectTab(tabId);
},
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Dropdown/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const iconClasses = cva({
<div v-if="icon" class="flex size-5 items-center justify-center p-1">
<Icon :name="icon" :class="iconClasses" />
</div>
<div class="col-start-2 px-2">
<div class="px-2" :class="icon ? 'col-start-2' : 'col-span-full'">
<slot v-if="hasDefaultSlot" />
<template v-else>{{ text }}</template>
</div>
Expand Down
95 changes: 85 additions & 10 deletions resources/js/components/ui/Publish/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import {
TabList,
TabTrigger,
TabProvider,
Button,
Dropdown,
DropdownMenu,
DropdownItem,
} from '@ui';
import TabContent from './TabContent.vue';
import { injectContainerContext } from './Container.vue';
import Sections from './Sections.vue';
import { ref, computed, useSlots, onMounted, watch } from 'vue';
import { ref, computed, useSlots, onMounted, onUnmounted, nextTick, watch } from 'vue';
import ElementContainer from '@/components/ElementContainer.vue';
import ShowField from '@/components/field-conditions/ShowField.js';
import { createTabsOverflowTracker } from '@/util/tabs-overflow.js';

const slots = useSlots();
const { blueprint, visibleValues, extraValues, revealerValues, errors, hiddenFields, setHiddenField, container, rememberTab } = injectContainerContext();
Expand Down Expand Up @@ -103,21 +108,91 @@ const tabsWithErrors = computed(() => {
function tabHasError(tab) {
return tabsWithErrors.value.includes(tab.handle);
}

const tabWrapper = ref(null);
const tabInner = ref(null);
const hasOverflow = ref(false);
const overflowedTabs = ref([]);
const overflowTracker = createTabsOverflowTracker({
getWrapper: () => tabWrapper.value,
getInner: () => tabInner.value,
getItems: () => visibleMainTabs.value,
onUpdate: ({ hasOverflow: nextHasOverflow, overflowedItems }) => {
hasOverflow.value = nextHasOverflow;
overflowedTabs.value = overflowedItems;
},
});

function checkOverflow() {
overflowTracker.checkOverflow();
}

watch(tabWrapper, (el) => {
if (el) {
overflowTracker.observe();
nextTick(checkOverflow);
}
});

watch(visibleMainTabs, () => {
nextTick(checkOverflow);
}, { deep: true });

onUnmounted(() => {
overflowTracker.disconnect();
});
</script>

<template>
<ElementContainer @resized="width = $event.width">
<div>
<Tabs v-if="width" v-model:modelValue="activeTab">
<TabList v-if="hasMultipleVisibleMainTabs" class="-mt-2 mb-6">
<TabTrigger
v-for="tab in visibleMainTabs"
:key="tab.handle"
:name="tab.handle"
:text="__(tab.display)"
:class="{ '!text-red-600': tabHasError(tab) }"
/>
</TabList>
<div v-if="hasMultipleVisibleMainTabs" class="flex items-center gap-x-2 -mt-2 mb-6">
<TabList class="flex-1 min-w-0 overflow-x-clip overflow-y-visible pe-0.25">
<div class="flex-1 flex items-center gap-x-2.5 min-w-0">
<div ref="tabWrapper" class="min-w-0 flex-1 flex overflow-clip px-0.25">
<div ref="tabInner" class="flex items-center gap-x-2.5 shrink-0">
<TabTrigger
v-for="tab in visibleMainTabs"
:key="tab.handle"
:name="tab.handle"
:class="{ '!text-red-600': tabHasError(tab) }"
>
<span class="block max-w-48 overflow-clip text-ellipsis whitespace-nowrap" v-tooltip="__(tab.display).length > 24 ? __(tab.display) : null">{{ __(tab.display) }}</span>
</TabTrigger>
</div>
</div>
<Dropdown
v-if="overflowedTabs.length"
align="end"
side="bottom"
class="shrink-0"
>
<template #trigger>
<Button
icon="dots"
variant="ghost"
size="sm"
:aria-label="__('Open dropdown menu')"
/>
</template>
<DropdownMenu>
<DropdownItem
v-for="tab in overflowedTabs"
:key="tab.handle"
:icon="tab.icon"
:class="{ 'bg-gray-100 dark:bg-gray-800': activeTab === tab.handle }"
@click="setActive(tab.handle)"
>
<span class="block max-w-48 overflow-hidden text-ellipsis whitespace-nowrap">
{{ __(tab.display) }}
</span>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</TabList>
</div>

<div :class="{ 'grid grid-cols-[1fr_320px] gap-8': shouldShowSidebar }">
<component
Expand Down
Loading