mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 21:37:32 -08:00
refactor(editor): Continue porting components over to composition API (no-changelog) (#8893)
This commit is contained in:
parent
80c4bc443a
commit
6c693e1afd
|
@ -19,6 +19,7 @@
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import N8nText from '../N8nText';
|
import N8nText from '../N8nText';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
|
import type { IconSize } from '@/types/icon';
|
||||||
|
|
||||||
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
|
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
|
||||||
export type CalloutTheme = (typeof THEMES)[number];
|
export type CalloutTheme = (typeof THEMES)[number];
|
||||||
|
@ -33,7 +34,7 @@ const CALLOUT_DEFAULT_ICONS = {
|
||||||
interface CalloutProps {
|
interface CalloutProps {
|
||||||
theme: CalloutTheme;
|
theme: CalloutTheme;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconSize?: string;
|
iconSize?: IconSize;
|
||||||
iconless?: boolean;
|
iconless?: boolean;
|
||||||
slim?: boolean;
|
slim?: boolean;
|
||||||
roundCorners?: boolean;
|
roundCorners?: boolean;
|
||||||
|
@ -58,7 +59,7 @@ const getIcon = computed(
|
||||||
() => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info,
|
() => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getIconSize = computed(() => {
|
const getIconSize = computed<IconSize>(() => {
|
||||||
if (props.iconSize) {
|
if (props.iconSize) {
|
||||||
return props.iconSize;
|
return props.iconSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,11 +34,11 @@
|
||||||
<slot v-if="hasDefaultSlot" />
|
<slot v-if="hasDefaultSlot" />
|
||||||
<N8nSelect
|
<N8nSelect
|
||||||
v-else-if="type === 'select' || type === 'multi-select'"
|
v-else-if="type === 'select' || type === 'multi-select'"
|
||||||
|
ref="inputRef"
|
||||||
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
|
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:multiple="type === 'multi-select'"
|
:multiple="type === 'multi-select'"
|
||||||
ref="inputRef"
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:name="name"
|
:name="name"
|
||||||
:teleported="teleported"
|
:teleported="teleported"
|
||||||
|
@ -57,8 +57,8 @@
|
||||||
</N8nSelect>
|
</N8nSelect>
|
||||||
<N8nInput
|
<N8nInput
|
||||||
v-else
|
v-else
|
||||||
:name="name"
|
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
|
:name="name"
|
||||||
:type="type"
|
:type="type"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import N8nFormInput from '../N8nFormInput';
|
import N8nFormInput from '../N8nFormInput';
|
||||||
import type { IFormInput } from '../../types';
|
import type { IFormInput, Validatable } from '../../types';
|
||||||
import ResizeObserver from '../ResizeObserver';
|
import ResizeObserver from '../ResizeObserver';
|
||||||
import type { EventBus } from '../../utils';
|
import type { EventBus } from '../../utils';
|
||||||
import { createEventBus } from '../../utils';
|
import { createEventBus } from '../../utils';
|
||||||
|
@ -83,7 +83,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showValidationWarnings: false,
|
showValidationWarnings: false,
|
||||||
values: {} as { [key: string]: unknown },
|
values: {} as { [key: string]: Validatable },
|
||||||
validity: {} as { [key: string]: boolean },
|
validity: {} as { [key: string]: boolean },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -141,12 +141,15 @@ export default defineComponent({
|
||||||
this.showValidationWarnings = true;
|
this.showValidationWarnings = true;
|
||||||
|
|
||||||
if (this.isReadyToSubmit) {
|
if (this.isReadyToSubmit) {
|
||||||
const toSubmit = this.filteredInputs.reduce<{ [key: string]: unknown }>((accu, input) => {
|
const toSubmit = this.filteredInputs.reduce(
|
||||||
if (this.values[input.name]) {
|
(accu, input) => {
|
||||||
accu[input.name] = this.values[input.name];
|
if (this.values[input.name]) {
|
||||||
}
|
accu[input.name] = this.values[input.name];
|
||||||
return accu;
|
}
|
||||||
}, {});
|
return accu;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: Validatable },
|
||||||
|
);
|
||||||
this.$emit('submit', toSubmit);
|
this.$emit('submit', toSubmit);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import N8nText from '../N8nText';
|
import N8nText from '../N8nText';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
import type { IconColor } from '@/types/icon';
|
import type { IconColor } from '@/types/icon';
|
||||||
|
@ -70,16 +70,16 @@ const props = withDefaults(defineProps<InfoAccordionProps>(), {
|
||||||
});
|
});
|
||||||
const $emit = defineEmits(['click:body', 'tooltipClick']);
|
const $emit = defineEmits(['click:body', 'tooltipClick']);
|
||||||
|
|
||||||
let expanded = false;
|
const expanded = ref(false);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.eventBus.on('expand', () => {
|
props.eventBus.on('expand', () => {
|
||||||
expanded = true;
|
expanded.value = true;
|
||||||
});
|
});
|
||||||
expanded = props.initiallyExpanded;
|
expanded.value = props.initiallyExpanded;
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
expanded = !expanded;
|
expanded.value = !expanded.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClick = (e: MouseEvent) => $emit('click:body', e);
|
const onClick = (e: MouseEvent) => $emit('click:body', e);
|
||||||
|
|
|
@ -9,15 +9,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
import N8nText from '../N8nText';
|
import N8nText from '../N8nText';
|
||||||
import N8nRoute, { type RouteTo } from '../N8nRoute';
|
import N8nRoute from '../N8nRoute';
|
||||||
import type { TextSize } from '@/types/text';
|
import type { TextSize } from '@/types/text';
|
||||||
|
|
||||||
const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
|
const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
|
||||||
|
|
||||||
interface LinkProps {
|
interface LinkProps {
|
||||||
|
to?: RouteLocationRaw;
|
||||||
size?: TextSize;
|
size?: TextSize;
|
||||||
to?: RouteTo;
|
|
||||||
newWindow?: boolean;
|
newWindow?: boolean;
|
||||||
bold?: boolean;
|
bold?: boolean;
|
||||||
underline?: boolean;
|
underline?: boolean;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
v-html="htmlContent"
|
v-html="htmlContent"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
<div v-for="(block, index) in loadingBlocks" :key="index">
|
<div v-for="(_, index) in loadingBlocks" :key="index">
|
||||||
<N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" />
|
<N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" />
|
||||||
<div :class="$style.spacer" />
|
<div :class="$style.spacer" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -53,104 +53,76 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { ElMenu } from 'element-plus';
|
import { ElMenu } from 'element-plus';
|
||||||
import N8nMenuItem from '../N8nMenuItem';
|
import N8nMenuItem from '../N8nMenuItem';
|
||||||
import type { PropType } from 'vue';
|
import type { IMenuItem } from '../../types';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import type { IMenuItem, RouteObject } from '../../types';
|
|
||||||
import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil';
|
import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil';
|
||||||
|
|
||||||
export default defineComponent({
|
interface MenuProps {
|
||||||
name: 'N8nMenu',
|
type?: 'primary' | 'secondary';
|
||||||
components: {
|
defaultActive?: string;
|
||||||
ElMenu,
|
collapsed?: boolean;
|
||||||
N8nMenuItem,
|
transparentBackground?: boolean;
|
||||||
},
|
mode?: 'router' | 'tabs';
|
||||||
props: {
|
tooltipDelay?: number;
|
||||||
type: {
|
items?: IMenuItem[];
|
||||||
type: String,
|
modelValue?: string;
|
||||||
default: 'primary',
|
}
|
||||||
validator: (value: string): boolean => ['primary', 'secondary'].includes(value),
|
|
||||||
},
|
|
||||||
defaultActive: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
collapsed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
transparentBackground: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'router',
|
|
||||||
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
|
|
||||||
},
|
|
||||||
tooltipDelay: {
|
|
||||||
type: Number,
|
|
||||||
default: 300,
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<IMenuItem[]>,
|
|
||||||
default: (): IMenuItem[] => [],
|
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
activeTab: this.value,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
upperMenuItems(): IMenuItem[] {
|
|
||||||
return this.items.filter(
|
|
||||||
(item: IMenuItem) => item.position === 'top' && item.available !== false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
lowerMenuItems(): IMenuItem[] {
|
|
||||||
return this.items.filter(
|
|
||||||
(item: IMenuItem) => item.position === 'bottom' && item.available !== false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
currentRoute(): RouteObject {
|
|
||||||
return (
|
|
||||||
this.$route || {
|
|
||||||
name: '',
|
|
||||||
path: '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.mode === 'router') {
|
|
||||||
const found = this.items.find((item) =>
|
|
||||||
doesMenuItemMatchCurrentRoute(item, this.currentRoute),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.activeTab = found ? found.id : '';
|
const props = withDefaults(defineProps<MenuProps>(), {
|
||||||
} else {
|
type: 'primary',
|
||||||
this.activeTab = this.items.length > 0 ? this.items[0].id : '';
|
collapsed: false,
|
||||||
}
|
transparentBackground: false,
|
||||||
|
mode: 'router',
|
||||||
this.$emit('update:modelValue', this.activeTab);
|
tooltipDelay: 300,
|
||||||
},
|
items: () => [],
|
||||||
methods: {
|
|
||||||
onSelect(item: IMenuItem): void {
|
|
||||||
if (this.mode === 'tabs') {
|
|
||||||
this.activeTab = item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('select', item.id);
|
|
||||||
this.$emit('update:modelValue', item.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
const $route = useRoute();
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'select', itemId: string);
|
||||||
|
(event: 'update:modelValue', itemId: string);
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const activeTab = ref(props.modelValue);
|
||||||
|
|
||||||
|
const upperMenuItems = computed(() =>
|
||||||
|
props.items.filter((item: IMenuItem) => item.position === 'top' && item.available !== false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lowerMenuItems = computed(() =>
|
||||||
|
props.items.filter((item: IMenuItem) => item.position === 'bottom' && item.available !== false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRoute = computed(() => {
|
||||||
|
return $route ?? { name: '', path: '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.mode === 'router') {
|
||||||
|
const found = props.items.find((item) =>
|
||||||
|
doesMenuItemMatchCurrentRoute(item, currentRoute.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
activeTab.value = found ? found.id : '';
|
||||||
|
} else {
|
||||||
|
activeTab.value = props.items.length > 0 ? props.items[0].id : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$emit('update:modelValue', activeTab.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSelect = (item: IMenuItem): void => {
|
||||||
|
if (props.mode === 'tabs') {
|
||||||
|
activeTab.value = item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$emit('select', item.id);
|
||||||
|
$emit('update:modelValue', item.id);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
}"
|
}"
|
||||||
data-test-id="menu-item"
|
data-test-id="menu-item"
|
||||||
:index="item.id"
|
:index="item.id"
|
||||||
@click="handleSelect(item)"
|
@click="handleSelect?.(item)"
|
||||||
>
|
>
|
||||||
<N8nIcon
|
<N8nIcon
|
||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
|
@ -80,94 +80,67 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { computed, useCssModule } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { ElSubMenu, ElMenuItem } from 'element-plus';
|
import { ElSubMenu, ElMenuItem } from 'element-plus';
|
||||||
import N8nTooltip from '../N8nTooltip';
|
import N8nTooltip from '../N8nTooltip';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import ConditionalRouterLink from '../ConditionalRouterLink';
|
import ConditionalRouterLink from '../ConditionalRouterLink';
|
||||||
import type { IMenuItem, RouteObject } from '../../types';
|
import type { IMenuItem } from '../../types';
|
||||||
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
|
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
|
||||||
|
|
||||||
export default defineComponent({
|
interface MenuItemProps {
|
||||||
name: 'N8nMenuItem',
|
item: IMenuItem;
|
||||||
components: {
|
compact?: boolean;
|
||||||
ElSubMenu,
|
tooltipDelay?: number;
|
||||||
ElMenuItem,
|
popperClass?: string;
|
||||||
N8nIcon,
|
mode?: 'router' | 'tabs';
|
||||||
N8nTooltip,
|
activeTab?: string;
|
||||||
ConditionalRouterLink,
|
handleSelect?: (item: IMenuItem) => void;
|
||||||
},
|
}
|
||||||
props: {
|
|
||||||
item: {
|
const props = withDefaults(defineProps<MenuItemProps>(), {
|
||||||
type: Object as PropType<IMenuItem>,
|
compact: false,
|
||||||
required: true,
|
tooltipDelay: 300,
|
||||||
},
|
popperClass: '',
|
||||||
compact: {
|
mode: 'router',
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
tooltipDelay: {
|
|
||||||
type: Number,
|
|
||||||
default: 300,
|
|
||||||
},
|
|
||||||
popperClass: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'router',
|
|
||||||
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
|
|
||||||
},
|
|
||||||
activeTab: {
|
|
||||||
type: String,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
handleSelect: {
|
|
||||||
type: Function as PropType<(item: IMenuItem) => void>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
availableChildren(): IMenuItem[] {
|
|
||||||
return Array.isArray(this.item.children)
|
|
||||||
? this.item.children.filter((child) => child.available !== false)
|
|
||||||
: [];
|
|
||||||
},
|
|
||||||
currentRoute(): RouteObject {
|
|
||||||
return (
|
|
||||||
this.$route || {
|
|
||||||
name: '',
|
|
||||||
path: '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
submenuPopperClass(): string {
|
|
||||||
const popperClass = [this.$style.submenuPopper, this.popperClass];
|
|
||||||
if (this.compact) {
|
|
||||||
popperClass.push(this.$style.compact);
|
|
||||||
}
|
|
||||||
return popperClass.join(' ');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isItemActive(item: IMenuItem): boolean {
|
|
||||||
const isItemActive = this.isActive(item);
|
|
||||||
const hasActiveChild =
|
|
||||||
Array.isArray(item.children) && item.children.some((child) => this.isActive(child));
|
|
||||||
return isItemActive || hasActiveChild;
|
|
||||||
},
|
|
||||||
isActive(item: IMenuItem): boolean {
|
|
||||||
if (this.mode === 'router') {
|
|
||||||
return doesMenuItemMatchCurrentRoute(item, this.currentRoute);
|
|
||||||
} else {
|
|
||||||
return item.id === this.activeTab;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $style = useCssModule();
|
||||||
|
const $route = useRoute();
|
||||||
|
|
||||||
|
const availableChildren = computed((): IMenuItem[] =>
|
||||||
|
Array.isArray(props.item.children)
|
||||||
|
? props.item.children.filter((child) => child.available !== false)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRoute = computed(() => {
|
||||||
|
return $route ?? { name: '', path: '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
const submenuPopperClass = computed((): string => {
|
||||||
|
const popperClass = [$style.submenuPopper, props.popperClass];
|
||||||
|
if (props.compact) {
|
||||||
|
popperClass.push($style.compact);
|
||||||
|
}
|
||||||
|
return popperClass.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const isActive = (item: IMenuItem): boolean => {
|
||||||
|
if (props.mode === 'router') {
|
||||||
|
return doesMenuItemMatchCurrentRoute(item, currentRoute.value);
|
||||||
|
} else {
|
||||||
|
return item.id === props.activeTab;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isItemActive = (item: IMenuItem): boolean => {
|
||||||
|
const hasActiveChild =
|
||||||
|
Array.isArray(item.children) && item.children.some((child) => isActive(child));
|
||||||
|
return isActive(item) || hasActiveChild;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import type { IMenuItem, RouteObject } from '@/types';
|
import type { IMenuItem } from '@/types';
|
||||||
import type { RouteLocationRaw } from 'vue-router';
|
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from 'vue-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given menu item matches the current route.
|
* Checks if the given menu item matches the current route.
|
||||||
*/
|
*/
|
||||||
export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: RouteObject) {
|
export function doesMenuItemMatchCurrentRoute(
|
||||||
|
item: IMenuItem,
|
||||||
|
currentRoute: RouteLocationNormalizedLoaded,
|
||||||
|
) {
|
||||||
let activateOnRouteNames: string[] = [];
|
let activateOnRouteNames: string[] = [];
|
||||||
if (Array.isArray(item.activateOnRouteNames)) {
|
if (Array.isArray(item.activateOnRouteNames)) {
|
||||||
activateOnRouteNames = item.activateOnRouteNames;
|
activateOnRouteNames = item.activateOnRouteNames;
|
||||||
|
@ -20,7 +23,7 @@ export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: Rou
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
activateOnRouteNames.includes(currentRoute.name ?? '') ||
|
activateOnRouteNames.includes((currentRoute.name as string) ?? '') ||
|
||||||
activateOnRoutePaths.includes(currentRoute.path)
|
activateOnRoutePaths.includes(currentRoute.path)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,99 +35,75 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { defineComponent, type PropType } from 'vue';
|
|
||||||
import type { Placement } from 'element-plus';
|
import type { Placement } from 'element-plus';
|
||||||
import N8nTooltip from '../N8nTooltip';
|
import N8nTooltip from '../N8nTooltip';
|
||||||
|
|
||||||
export default defineComponent({
|
interface NodeIconProps {
|
||||||
name: 'N8nNodeIcon',
|
type: 'file' | 'icon' | 'unknown';
|
||||||
components: {
|
src?: string;
|
||||||
N8nTooltip,
|
name?: string;
|
||||||
FontAwesomeIcon,
|
nodeTypeName?: string;
|
||||||
},
|
size?: number;
|
||||||
props: {
|
disabled?: boolean;
|
||||||
type: {
|
circle?: boolean;
|
||||||
type: String,
|
color?: string;
|
||||||
required: true,
|
showTooltip?: boolean;
|
||||||
validator: (value: string): boolean => ['file', 'icon', 'unknown'].includes(value),
|
tooltipPosition?: Placement;
|
||||||
},
|
badge?: { src: string; type: string };
|
||||||
src: {
|
}
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
nodeTypeName: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
circle: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
showTooltip: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
tooltipPosition: {
|
|
||||||
type: String as PropType<Placement>,
|
|
||||||
default: 'top',
|
|
||||||
},
|
|
||||||
badge: { type: Object as PropType<{ src: string; type: string }> },
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
iconStyleData(): Record<string, string> {
|
|
||||||
if (!this.size) {
|
|
||||||
return {
|
|
||||||
color: this.color || '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const props = withDefaults(defineProps<NodeIconProps>(), {
|
||||||
color: this.color || '',
|
tooltipPosition: 'top',
|
||||||
width: `${this.size}px`,
|
});
|
||||||
height: `${this.size}px`,
|
|
||||||
'font-size': `${this.size}px`,
|
|
||||||
'line-height': `${this.size}px`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
badgeSize(): number {
|
|
||||||
switch (this.size) {
|
|
||||||
case 40:
|
|
||||||
return 18;
|
|
||||||
case 24:
|
|
||||||
return 10;
|
|
||||||
case 18:
|
|
||||||
default:
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
badgeStyleData(): Record<string, string> {
|
|
||||||
const size = this.badgeSize;
|
|
||||||
return {
|
|
||||||
padding: `${Math.floor(size / 4)}px`,
|
|
||||||
right: `-${Math.floor(size / 2)}px`,
|
|
||||||
bottom: `-${Math.floor(size / 2)}px`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
fontStyleData(): Record<string, string> {
|
|
||||||
if (!this.size) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const iconStyleData = computed((): Record<string, string> => {
|
||||||
'max-width': `${this.size}px`,
|
if (!props.size) {
|
||||||
};
|
return {
|
||||||
},
|
color: props.color || '',
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: props.color || '',
|
||||||
|
width: `${props.size}px`,
|
||||||
|
height: `${props.size}px`,
|
||||||
|
'font-size': `${props.size}px`,
|
||||||
|
'line-height': `${props.size}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeSize = computed((): number => {
|
||||||
|
switch (props.size) {
|
||||||
|
case 40:
|
||||||
|
return 18;
|
||||||
|
case 24:
|
||||||
|
return 10;
|
||||||
|
case 18:
|
||||||
|
default:
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fontStyleData = computed((): Record<string, string> => {
|
||||||
|
if (!props.size) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'max-width': `${props.size}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeStyleData = computed((): Record<string, string> => {
|
||||||
|
const size = badgeSize.value;
|
||||||
|
return {
|
||||||
|
padding: `${Math.floor(size / 4)}px`,
|
||||||
|
right: `-${Math.floor(size / 2)}px`,
|
||||||
|
bottom: `-${Math.floor(size / 2)}px`,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -58,18 +58,17 @@ Sanitized.args = {
|
||||||
'<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
|
'<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Truncated = PropTemplate.bind({});
|
export const FullContent = PropTemplate.bind({});
|
||||||
Truncated.args = {
|
FullContent.args = {
|
||||||
theme: 'warning',
|
theme: 'warning',
|
||||||
truncate: true,
|
content: 'This is just the summary. <a data-key="toggle-expand">Show more</a>',
|
||||||
content:
|
fullContent:
|
||||||
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ut labore et dolore magna aliqua. <a data-key="show-less">Show less</a>',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HtmlEdgeCase = PropTemplate.bind({});
|
export const HtmlEdgeCase = PropTemplate.bind({});
|
||||||
HtmlEdgeCase.args = {
|
HtmlEdgeCase.args = {
|
||||||
theme: 'warning',
|
theme: 'warning',
|
||||||
truncate: true,
|
|
||||||
content:
|
content:
|
||||||
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
|
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:id="`${id}-content`"
|
:id="`${id}-content`"
|
||||||
:class="showFullContent ? $style['expanded'] : $style['truncated']"
|
:class="showFullContent ? $style['expanded'] : $style['truncated']"
|
||||||
role="region"
|
role="region"
|
||||||
v-html="sanitizeHtml(showFullContent ? fullContent : content)"
|
v-html="displayContent"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
@ -15,89 +15,70 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, ref, useCssModule } from 'vue';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitize from 'sanitize-html';
|
||||||
import N8nText from '../../components/N8nText';
|
import N8nText from '../../components/N8nText';
|
||||||
import Locale from '../../mixins/locale';
|
|
||||||
import { uid } from '../../utils';
|
import { uid } from '../../utils';
|
||||||
|
|
||||||
export default defineComponent({
|
interface NoticeProps {
|
||||||
name: 'N8nNotice',
|
id?: string;
|
||||||
directives: {},
|
theme?: 'success' | 'warning' | 'danger' | 'info';
|
||||||
components: {
|
content?: string;
|
||||||
N8nText,
|
fullContent?: string;
|
||||||
},
|
}
|
||||||
mixins: [Locale],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
default: () => uid('notice'),
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
type: String,
|
|
||||||
default: 'warning',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
fullContent: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showFullContent: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
classes(): string[] {
|
|
||||||
return ['notice', this.$style.notice, this.$style[this.theme]];
|
|
||||||
},
|
|
||||||
canTruncate(): boolean {
|
|
||||||
return this.fullContent !== undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleExpanded() {
|
|
||||||
this.showFullContent = !this.showFullContent;
|
|
||||||
},
|
|
||||||
sanitizeHtml(text: string): string {
|
|
||||||
return sanitizeHtml(text, {
|
|
||||||
allowedAttributes: {
|
|
||||||
a: [
|
|
||||||
'data-key',
|
|
||||||
'href',
|
|
||||||
'target',
|
|
||||||
'data-action',
|
|
||||||
'data-action-parameter-connectiontype',
|
|
||||||
'data-action-parameter-creatorview',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onClick(event: MouseEvent) {
|
|
||||||
if (!(event.target instanceof HTMLElement)) return;
|
|
||||||
|
|
||||||
if (event.target.localName !== 'a') return;
|
const props = withDefaults(defineProps<NoticeProps>(), {
|
||||||
|
id: () => uid('notice'),
|
||||||
if (event.target.dataset?.key) {
|
theme: 'warning',
|
||||||
event.stopPropagation();
|
content: '',
|
||||||
event.preventDefault();
|
fullContent: '',
|
||||||
|
|
||||||
if (event.target.dataset.key === 'show-less') {
|
|
||||||
this.showFullContent = false;
|
|
||||||
} else if (this.canTruncate && event.target.dataset.key === 'toggle-expand') {
|
|
||||||
this.showFullContent = !this.showFullContent;
|
|
||||||
} else {
|
|
||||||
this.$emit('action', event.target.dataset.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'action', key: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const classes = computed(() => ['notice', $style.notice, $style[props.theme]]);
|
||||||
|
const canTruncate = computed(() => props.fullContent !== undefined);
|
||||||
|
|
||||||
|
const showFullContent = ref(false);
|
||||||
|
const displayContent = computed(() =>
|
||||||
|
sanitize(showFullContent.value ? props.fullContent : props.content, {
|
||||||
|
allowedAttributes: {
|
||||||
|
a: [
|
||||||
|
'data-key',
|
||||||
|
'href',
|
||||||
|
'target',
|
||||||
|
'data-action',
|
||||||
|
'data-action-parameter-connectiontype',
|
||||||
|
'data-action-parameter-creatorview',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClick = (event: MouseEvent) => {
|
||||||
|
if (!(event.target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
if (event.target.localName !== 'a') return;
|
||||||
|
|
||||||
|
const anchorKey = event.target.dataset?.key;
|
||||||
|
if (anchorKey) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (anchorKey === 'show-less') {
|
||||||
|
showFullContent.value = false;
|
||||||
|
} else if (canTruncate.value && anchorKey === 'toggle-expand') {
|
||||||
|
showFullContent.value = !showFullContent.value;
|
||||||
|
} else {
|
||||||
|
$emit('action', anchorKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -8,12 +8,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
defineOptions({ name: 'N8nPulse' });
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'N8nPulse',
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
[$style.container]: true,
|
[$style.container]: true,
|
||||||
[$style.hoverable]: !disabled,
|
[$style.hoverable]: !disabled,
|
||||||
}"
|
}"
|
||||||
aria-checked="true"
|
:aria-checked="active"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
|
@ -23,33 +23,19 @@
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
interface RadioButtonProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
size?: 'small' | 'medium';
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
withDefaults(defineProps<RadioButtonProps>(), {
|
||||||
name: 'N8nRadioButton',
|
active: false,
|
||||||
props: {
|
disabled: false,
|
||||||
label: {
|
size: 'medium',
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: 'medium',
|
|
||||||
validator: (value: string): boolean => ['small', 'medium'].includes(value),
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -54,3 +54,19 @@ Example.args = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Disabled = Template.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
modelValue: 'enabled',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Enabled',
|
||||||
|
value: 'enabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Disabled',
|
||||||
|
value: 'disabled',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
@ -15,48 +15,41 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import RadioButton from './RadioButton.vue';
|
import RadioButton from './RadioButton.vue';
|
||||||
|
|
||||||
import type { PropType } from 'vue';
|
interface RadioOption {
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export interface RadioOption {
|
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
interface RadioButtonsProps {
|
||||||
name: 'N8nRadioButtons',
|
modelValue?: string;
|
||||||
components: {
|
options?: RadioOption[];
|
||||||
RadioButton,
|
size: 'small' | 'medium';
|
||||||
},
|
disabled?: boolean;
|
||||||
props: {
|
}
|
||||||
modelValue: {
|
|
||||||
type: String,
|
const props = withDefaults(defineProps<RadioButtonsProps>(), {
|
||||||
},
|
active: false,
|
||||||
options: {
|
disabled: false,
|
||||||
type: Array as PropType<RadioOption[]>,
|
size: 'medium',
|
||||||
default: (): RadioOption[] => [],
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
methods: {
|
|
||||||
onClick(option: { label: string; value: string; disabled?: boolean }, event: MouseEvent) {
|
|
||||||
if (this.disabled || option.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$emit('update:modelValue', option.value, event);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string, e: MouseEvent): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onClick = (
|
||||||
|
option: { label: string; value: string; disabled?: boolean },
|
||||||
|
event: MouseEvent,
|
||||||
|
) => {
|
||||||
|
if (props.disabled || option.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$emit('update:modelValue', option.value, event);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import type { ComponentInstance } from 'vue';
|
||||||
import type { StoryFn } from '@storybook/vue3';
|
import type { StoryFn } from '@storybook/vue3';
|
||||||
import N8nRecycleScroller from './RecycleScroller.vue';
|
import N8nRecycleScroller from './RecycleScroller.vue';
|
||||||
import type { ComponentInstance } from 'vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/RecycleScroller',
|
title: 'Atoms/RecycleScroller',
|
||||||
|
@ -23,7 +23,9 @@ const Template: StoryFn = (args) => ({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resizeItem(item: { id: string; height: string }, fn: (item: { id: string }) => void) {
|
resizeItem(item: { id: string; height: string }, fn: (item: { id: string }) => void) {
|
||||||
const itemRef = (this as ComponentInstance).$refs[`item-${item.id}`] as HTMLElement;
|
const itemRef = (this as ComponentInstance<typeof N8nRecycleScroller>).$refs[
|
||||||
|
`item-${item.id}`
|
||||||
|
] as HTMLElement;
|
||||||
|
|
||||||
item.height = '200px';
|
item.height = '200px';
|
||||||
itemRef.style.height = '200px';
|
itemRef.style.height = '200px';
|
||||||
|
|
|
@ -1,225 +1,182 @@
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
import type { ComponentPublicInstance } from 'vue';
|
||||||
|
import { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
|
||||||
|
|
||||||
import type { PropType, ComponentPublicInstance } from 'vue';
|
interface RecycleScrollerProps {
|
||||||
import { computed, defineComponent, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
|
itemSize: number;
|
||||||
|
items: Array<Record<string, string>>;
|
||||||
|
itemKey: string;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<RecycleScrollerProps>(), {
|
||||||
name: 'N8nRecycleScroller',
|
offset: 2,
|
||||||
props: {
|
});
|
||||||
itemSize: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<Array<Record<string, string>>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
itemKey: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const wrapperRef = ref<HTMLElement | null>(null);
|
|
||||||
const scrollerRef = ref<HTMLElement | null>(null);
|
|
||||||
const itemsRef = ref<HTMLElement | null>(null);
|
|
||||||
const itemRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({});
|
|
||||||
|
|
||||||
const scrollTop = ref(0);
|
const wrapperRef = ref<HTMLElement | null>(null);
|
||||||
const wrapperHeight = ref(0);
|
const scrollerRef = ref<HTMLElement | null>(null);
|
||||||
const windowHeight = ref(0);
|
const itemsRef = ref<HTMLElement | null>(null);
|
||||||
|
const itemRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({});
|
||||||
|
|
||||||
const itemCount = computed(() => props.items.length);
|
const scrollTop = ref(0);
|
||||||
|
const wrapperHeight = ref(0);
|
||||||
|
const windowHeight = ref(0);
|
||||||
|
|
||||||
/**
|
/** Cache */
|
||||||
* Cache
|
|
||||||
*/
|
|
||||||
|
|
||||||
const itemSizeCache = ref<Record<string, number>>({});
|
const itemSizeCache = ref<Record<string, number>>({});
|
||||||
const itemPositionCache = computed(() => {
|
const itemPositionCache = computed(() => {
|
||||||
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
||||||
const key = item[props.itemKey];
|
const key = item[props.itemKey];
|
||||||
const prevItem = props.items[index - 1];
|
const prevItem = props.items[index - 1];
|
||||||
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
|
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
|
||||||
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
|
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
|
||||||
|
|
||||||
acc[key] = prevItemPosition + prevItemSize;
|
acc[key] = prevItemPosition + prevItemSize;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/** Indexes */
|
||||||
* Indexes
|
|
||||||
*/
|
|
||||||
|
|
||||||
const startIndex = computed(() => {
|
const startIndex = computed(() => {
|
||||||
const foundIndex =
|
const foundIndex =
|
||||||
props.items.findIndex((item) => {
|
props.items.findIndex((item) => {
|
||||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||||
|
|
||||||
return itemPosition >= scrollTop.value;
|
return itemPosition >= scrollTop.value;
|
||||||
}) - 1;
|
}) - 1;
|
||||||
const index = foundIndex - props.offset;
|
const index = foundIndex - props.offset;
|
||||||
|
|
||||||
return index < 0 ? 0 : index;
|
return index < 0 ? 0 : index;
|
||||||
});
|
});
|
||||||
|
|
||||||
const endIndex = computed(() => {
|
const endIndex = computed(() => {
|
||||||
const foundIndex = props.items.findIndex((item) => {
|
const foundIndex = props.items.findIndex((item) => {
|
||||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||||
const itemSize = itemSizeCache.value[item[props.itemKey]];
|
const itemSize = itemSizeCache.value[item[props.itemKey]];
|
||||||
|
|
||||||
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
|
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
|
||||||
});
|
});
|
||||||
const index = foundIndex + props.offset;
|
const index = foundIndex + props.offset;
|
||||||
|
|
||||||
return foundIndex === -1 ? props.items.length - 1 : index;
|
return foundIndex === -1 ? props.items.length - 1 : index;
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleItems = computed(() => {
|
const visibleItems = computed(() => {
|
||||||
return props.items.slice(startIndex.value, endIndex.value + 1);
|
return props.items.slice(startIndex.value, endIndex.value + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => visibleItems.value,
|
() => visibleItems.value,
|
||||||
(currentValue, previousValue) => {
|
(currentValue, previousValue) => {
|
||||||
const difference = currentValue.filter(
|
const difference = currentValue.filter(
|
||||||
(currentItem) =>
|
(currentItem) =>
|
||||||
!previousValue.find(
|
!previousValue.find(
|
||||||
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
|
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
|
|
||||||
if (difference.length > 0) {
|
|
||||||
updateItemSizeCache(difference);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
if (difference.length > 0) {
|
||||||
* Computed sizes and styles
|
updateItemSizeCache(difference);
|
||||||
*/
|
|
||||||
|
|
||||||
const scrollerHeight = computed(() => {
|
|
||||||
const lastItem = props.items[props.items.length - 1];
|
|
||||||
const lastItemPosition = lastItem ? itemPositionCache.value[lastItem[props.itemKey]] : 0;
|
|
||||||
const lastItemSize = lastItem ? itemSizeCache.value[lastItem[props.itemKey]] : props.itemSize;
|
|
||||||
|
|
||||||
return lastItemPosition + lastItemSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
const scrollerStyles = computed(() => ({
|
|
||||||
height: `${scrollerHeight.value}px`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const itemsStyles = computed(() => {
|
|
||||||
const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
|
|
||||||
|
|
||||||
return {
|
|
||||||
transform: `translateY(${offset}px)`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle hooks
|
|
||||||
*/
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
initializeItemSizeCache();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (wrapperRef.value) {
|
|
||||||
wrapperRef.value.addEventListener('scroll', onScroll);
|
|
||||||
updateItemSizeCache(visibleItems.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', onWindowResize);
|
|
||||||
onWindowResize();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
function initializeItemSizeCache() {
|
|
||||||
props.items.forEach((item) => {
|
|
||||||
itemSizeCache.value = {
|
|
||||||
...itemSizeCache.value,
|
|
||||||
[item[props.itemKey]]: props.itemSize,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function updateItemSizeCache(items: Array<Record<string, string>>) {
|
/** Computed sizes and styles */
|
||||||
for (const item of items) {
|
|
||||||
onUpdateItemSize(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUpdateItemSize(item: { [key: string]: string }) {
|
const scrollerHeight = computed(() => {
|
||||||
void nextTick(() => {
|
const lastItem = props.items[props.items.length - 1];
|
||||||
const itemId = item[props.itemKey];
|
const lastItemPosition = lastItem ? itemPositionCache.value[lastItem[props.itemKey]] : 0;
|
||||||
const itemRef = itemRefs.value[itemId] as HTMLElement;
|
const lastItemSize = lastItem ? itemSizeCache.value[lastItem[props.itemKey]] : props.itemSize;
|
||||||
const previousSize = itemSizeCache.value[itemId];
|
|
||||||
const size = itemRef ? itemRef.offsetHeight : props.itemSize;
|
|
||||||
const difference = size - previousSize;
|
|
||||||
|
|
||||||
itemSizeCache.value = {
|
return lastItemPosition + lastItemSize;
|
||||||
...itemSizeCache.value,
|
});
|
||||||
[item[props.itemKey]]: size,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (wrapperRef.value && scrollTop.value) {
|
const scrollerStyles = computed(() => ({
|
||||||
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference;
|
height: `${scrollerHeight.value}px`,
|
||||||
scrollTop.value = wrapperRef.value.scrollTop;
|
}));
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize() {
|
const itemsStyles = computed(() => {
|
||||||
if (wrapperRef.value) {
|
const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
|
||||||
wrapperHeight.value = wrapperRef.value.offsetHeight;
|
|
||||||
void nextTick(() => {
|
|
||||||
updateItemSizeCache(visibleItems.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
windowHeight.value = window.innerHeight;
|
return {
|
||||||
}
|
transform: `translateY(${offset}px)`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function onScroll() {
|
/** Lifecycle hooks */
|
||||||
if (!wrapperRef.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initializeItemSizeCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (wrapperRef.value) {
|
||||||
|
wrapperRef.value.addEventListener('scroll', onScroll);
|
||||||
|
updateItemSizeCache(visibleItems.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
onWindowResize();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Event handlers */
|
||||||
|
|
||||||
|
function initializeItemSizeCache() {
|
||||||
|
props.items.forEach((item) => {
|
||||||
|
itemSizeCache.value = {
|
||||||
|
...itemSizeCache.value,
|
||||||
|
[item[props.itemKey]]: props.itemSize,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemSizeCache(items: Array<Record<string, string>>) {
|
||||||
|
for (const item of items) {
|
||||||
|
onUpdateItemSize(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateItemSize(item: { [key: string]: string }) {
|
||||||
|
void nextTick(() => {
|
||||||
|
const itemId = item[props.itemKey];
|
||||||
|
const itemRef = itemRefs.value[itemId] as HTMLElement;
|
||||||
|
const previousSize = itemSizeCache.value[itemId];
|
||||||
|
const size = itemRef ? itemRef.offsetHeight : props.itemSize;
|
||||||
|
const difference = size - previousSize;
|
||||||
|
|
||||||
|
itemSizeCache.value = {
|
||||||
|
...itemSizeCache.value,
|
||||||
|
[item[props.itemKey]]: size,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (wrapperRef.value && scrollTop.value) {
|
||||||
|
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference;
|
||||||
scrollTop.value = wrapperRef.value.scrollTop;
|
scrollTop.value = wrapperRef.value.scrollTop;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
function onWindowResize() {
|
||||||
startIndex,
|
if (wrapperRef.value) {
|
||||||
endIndex,
|
wrapperHeight.value = wrapperRef.value.offsetHeight;
|
||||||
itemCount,
|
void nextTick(() => {
|
||||||
itemSizeCache,
|
updateItemSizeCache(visibleItems.value);
|
||||||
itemPositionCache,
|
});
|
||||||
itemsVisible: visibleItems,
|
}
|
||||||
itemsStyles,
|
|
||||||
scrollerStyles,
|
windowHeight.value = window.innerHeight;
|
||||||
scrollerScrollTop: scrollTop,
|
}
|
||||||
scrollerRef,
|
|
||||||
wrapperRef,
|
function onScroll() {
|
||||||
itemsRef,
|
if (!wrapperRef.value) {
|
||||||
itemRefs,
|
return;
|
||||||
onUpdateItemSize,
|
}
|
||||||
};
|
|
||||||
},
|
scrollTop.value = wrapperRef.value.scrollTop;
|
||||||
});
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -227,7 +184,7 @@ export default defineComponent({
|
||||||
<div ref="scrollerRef" class="recycle-scroller" :style="scrollerStyles">
|
<div ref="scrollerRef" class="recycle-scroller" :style="scrollerStyles">
|
||||||
<div ref="itemsRef" class="recycle-scroller-items-wrapper" :style="itemsStyles">
|
<div ref="itemsRef" class="recycle-scroller-items-wrapper" :style="itemsStyles">
|
||||||
<div
|
<div
|
||||||
v-for="item in itemsVisible"
|
v-for="item in visibleItems"
|
||||||
:key="item[itemKey]"
|
:key="item[itemKey]"
|
||||||
:ref="(element) => (itemRefs[item[itemKey]] = element)"
|
:ref="(element) => (itemRefs[item[itemKey]] = element)"
|
||||||
class="recycle-scroller-item"
|
class="recycle-scroller-item"
|
||||||
|
|
|
@ -3,7 +3,7 @@ import N8nRecycleScroller from '../RecycleScroller.vue';
|
||||||
|
|
||||||
const itemSize = 100;
|
const itemSize = 100;
|
||||||
const itemKey = 'id';
|
const itemKey = 'id';
|
||||||
const items = [...(new Array(100) as number[])].map((item, index) => ({
|
const items = [...(new Array(100) as number[])].map((_, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
name: `Item ${index}`,
|
name: `Item ${index}`,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -11,9 +11,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
function closestNumber(value: number, divisor: number): number {
|
function closestNumber(value: number, divisor: number): number {
|
||||||
const q = value / divisor;
|
const q = value / divisor;
|
||||||
|
@ -35,7 +34,7 @@ function getSize(min: number, virtual: number, gridSize: number): number {
|
||||||
return min;
|
return min;
|
||||||
}
|
}
|
||||||
|
|
||||||
const directionsCursorMaps: { [key: string]: string } = {
|
const directionsCursorMaps = {
|
||||||
right: 'ew-resize',
|
right: 'ew-resize',
|
||||||
top: 'ns-resize',
|
top: 'ns-resize',
|
||||||
bottom: 'ns-resize',
|
bottom: 'ns-resize',
|
||||||
|
@ -44,141 +43,141 @@ const directionsCursorMaps: { [key: string]: string } = {
|
||||||
topRight: 'ne-resize',
|
topRight: 'ne-resize',
|
||||||
bottomLeft: 'sw-resize',
|
bottomLeft: 'sw-resize',
|
||||||
bottomRight: 'se-resize',
|
bottomRight: 'se-resize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Direction = keyof typeof directionsCursorMaps;
|
||||||
|
|
||||||
|
interface ResizeProps {
|
||||||
|
isResizingEnabled?: boolean;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
scale?: number;
|
||||||
|
gridSize?: number;
|
||||||
|
supportedDirections?: Direction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ResizeProps>(), {
|
||||||
|
isResizingEnabled: true,
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
scale: 1,
|
||||||
|
gridSize: 20,
|
||||||
|
supportedDirections: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ResizeData {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
dX: number;
|
||||||
|
dY: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
direction: Direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'resizestart');
|
||||||
|
(event: 'resize', value: ResizeData): void;
|
||||||
|
(event: 'resizeend');
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const enabledDirections = computed((): Direction[] => {
|
||||||
|
const availableDirections = Object.keys(directionsCursorMaps) as Direction[];
|
||||||
|
|
||||||
|
if (!props.isResizingEnabled) return [];
|
||||||
|
if (props.supportedDirections.length === 0) return availableDirections;
|
||||||
|
|
||||||
|
return props.supportedDirections;
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
dir: ref<Direction | ''>(''),
|
||||||
|
dHeight: ref(0),
|
||||||
|
dWidth: ref(0),
|
||||||
|
vHeight: ref(0),
|
||||||
|
vWidth: ref(0),
|
||||||
|
x: ref(0),
|
||||||
|
y: ref(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const mouseMove = (event: MouseEvent) => {
|
||||||
name: 'N8nResize',
|
event.preventDefault();
|
||||||
props: {
|
event.stopPropagation();
|
||||||
isResizingEnabled: {
|
let dWidth = 0;
|
||||||
type: Boolean,
|
let dHeight = 0;
|
||||||
default: true,
|
let top = false;
|
||||||
},
|
let left = false;
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
minHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
minWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
gridSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 20,
|
|
||||||
},
|
|
||||||
supportedDirections: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: (): string[] => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
directionsCursorMaps,
|
|
||||||
dir: '',
|
|
||||||
dHeight: 0,
|
|
||||||
dWidth: 0,
|
|
||||||
vHeight: 0,
|
|
||||||
vWidth: 0,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
enabledDirections(): string[] {
|
|
||||||
const availableDirections = Object.keys(directionsCursorMaps);
|
|
||||||
|
|
||||||
if (!this.isResizingEnabled) return [];
|
if (state.dir.value.includes('right')) {
|
||||||
if (this.supportedDirections.length === 0) return availableDirections;
|
dWidth = event.pageX - state.x.value;
|
||||||
|
}
|
||||||
|
if (state.dir.value.includes('left')) {
|
||||||
|
dWidth = state.x.value - event.pageX;
|
||||||
|
left = true;
|
||||||
|
}
|
||||||
|
if (state.dir.value.includes('top')) {
|
||||||
|
dHeight = state.y.value - event.pageY;
|
||||||
|
top = true;
|
||||||
|
}
|
||||||
|
if (state.dir.value.includes('bottom')) {
|
||||||
|
dHeight = event.pageY - state.y.value;
|
||||||
|
}
|
||||||
|
|
||||||
return this.supportedDirections;
|
const deltaWidth = (dWidth - state.dWidth.value) / props.scale;
|
||||||
},
|
const deltaHeight = (dHeight - state.dHeight.value) / props.scale;
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resizerMove(event: MouseEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const targetResizer = event.target as { dataset: { dir: string } } | null;
|
state.vHeight.value = state.vHeight.value + deltaHeight;
|
||||||
if (targetResizer) {
|
state.vWidth.value = state.vWidth.value + deltaWidth;
|
||||||
this.dir = targetResizer.dataset.dir.toLocaleLowerCase();
|
const height = getSize(props.minHeight, state.vHeight.value, props.gridSize);
|
||||||
}
|
const width = getSize(props.minWidth, state.vWidth.value, props.gridSize);
|
||||||
|
|
||||||
document.body.style.cursor = directionsCursorMaps[this.dir];
|
const dX = left && width !== props.width ? -1 * (width - props.width) : 0;
|
||||||
|
const dY = top && height !== props.height ? -1 * (height - props.height) : 0;
|
||||||
|
const x = event.x;
|
||||||
|
const y = event.y;
|
||||||
|
const direction = state.dir.value as Direction;
|
||||||
|
|
||||||
this.x = event.pageX;
|
$emit('resize', { height, width, dX, dY, x, y, direction });
|
||||||
this.y = event.pageY;
|
state.dHeight.value = dHeight;
|
||||||
this.dWidth = 0;
|
state.dWidth.value = dWidth;
|
||||||
this.dHeight = 0;
|
};
|
||||||
this.vHeight = this.height;
|
|
||||||
this.vWidth = this.width;
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', this.mouseMove);
|
const mouseUp = (event: MouseEvent) => {
|
||||||
window.addEventListener('mouseup', this.mouseUp);
|
event.preventDefault();
|
||||||
this.$emit('resizestart');
|
event.stopPropagation();
|
||||||
},
|
$emit('resizeend');
|
||||||
mouseMove(event: MouseEvent) {
|
window.removeEventListener('mousemove', mouseMove);
|
||||||
event.preventDefault();
|
window.removeEventListener('mouseup', mouseUp);
|
||||||
event.stopPropagation();
|
document.body.style.cursor = 'unset';
|
||||||
let dWidth = 0;
|
state.dir.value = '';
|
||||||
let dHeight = 0;
|
};
|
||||||
let top = false;
|
|
||||||
let left = false;
|
|
||||||
|
|
||||||
if (this.dir.includes('right')) {
|
const resizerMove = (event: MouseEvent) => {
|
||||||
dWidth = event.pageX - this.x;
|
event.preventDefault();
|
||||||
}
|
event.stopPropagation();
|
||||||
if (this.dir.includes('left')) {
|
|
||||||
dWidth = this.x - event.pageX;
|
|
||||||
left = true;
|
|
||||||
}
|
|
||||||
if (this.dir.includes('top')) {
|
|
||||||
dHeight = this.y - event.pageY;
|
|
||||||
top = true;
|
|
||||||
}
|
|
||||||
if (this.dir.includes('bottom')) {
|
|
||||||
dHeight = event.pageY - this.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deltaWidth = (dWidth - this.dWidth) / this.scale;
|
const targetResizer = event.target as { dataset: { dir: string } } | null;
|
||||||
const deltaHeight = (dHeight - this.dHeight) / this.scale;
|
if (targetResizer) {
|
||||||
|
state.dir.value = targetResizer.dataset.dir.toLocaleLowerCase() as Direction;
|
||||||
|
}
|
||||||
|
|
||||||
this.vHeight = this.vHeight + deltaHeight;
|
document.body.style.cursor = directionsCursorMaps[state.dir.value];
|
||||||
this.vWidth = this.vWidth + deltaWidth;
|
|
||||||
const height = getSize(this.minHeight, this.vHeight, this.gridSize);
|
|
||||||
const width = getSize(this.minWidth, this.vWidth, this.gridSize);
|
|
||||||
|
|
||||||
const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
|
state.x.value = event.pageX;
|
||||||
const dY = top && height !== this.height ? -1 * (height - this.height) : 0;
|
state.y.value = event.pageY;
|
||||||
const x = event.x;
|
state.dWidth.value = 0;
|
||||||
const y = event.y;
|
state.dHeight.value = 0;
|
||||||
const direction = this.dir;
|
state.vHeight.value = props.height;
|
||||||
|
state.vWidth.value = props.width;
|
||||||
|
|
||||||
this.$emit('resize', { height, width, dX, dY, x, y, direction });
|
window.addEventListener('mousemove', mouseMove);
|
||||||
this.dHeight = dHeight;
|
window.addEventListener('mouseup', mouseUp);
|
||||||
this.dWidth = dWidth;
|
$emit('resizestart');
|
||||||
},
|
};
|
||||||
mouseUp(event: MouseEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.$emit('resizeend');
|
|
||||||
window.removeEventListener('mousemove', this.mouseMove);
|
|
||||||
window.removeEventListener('mouseup', this.mouseUp);
|
|
||||||
document.body.style.cursor = 'unset';
|
|
||||||
this.dir = '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -9,12 +9,10 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { type RouteLocationRaw } from 'vue-router';
|
||||||
// TODO: replace `object` with a more detailed type
|
|
||||||
export type RouteTo = string | object;
|
|
||||||
|
|
||||||
interface RouteProps {
|
interface RouteProps {
|
||||||
to?: RouteTo;
|
to?: RouteLocationRaw;
|
||||||
newWindow?: boolean;
|
newWindow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
import N8nRoute from './Route.vue';
|
import N8nRoute from './Route.vue';
|
||||||
export type { RouteTo } from './Route.vue';
|
|
||||||
export default N8nRoute;
|
export default N8nRoute;
|
||||||
|
|
|
@ -30,16 +30,11 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ElSelect } from 'element-plus';
|
import { ElSelect } from 'element-plus';
|
||||||
import { defineComponent } from 'vue';
|
import { type PropType, defineComponent } from 'vue';
|
||||||
|
import type { SelectSize } from '@/types';
|
||||||
|
|
||||||
type InnerSelectRef = InstanceType<typeof ElSelect>;
|
type InnerSelectRef = InstanceType<typeof ElSelect>;
|
||||||
|
|
||||||
export interface IProps {
|
|
||||||
size?: string;
|
|
||||||
limitPopperWidth?: string;
|
|
||||||
popperClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'N8nSelect',
|
name: 'N8nSelect',
|
||||||
components: {
|
components: {
|
||||||
|
@ -49,10 +44,8 @@ export default defineComponent({
|
||||||
...ElSelect.props,
|
...ElSelect.props,
|
||||||
modelValue: {},
|
modelValue: {},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String as PropType<SelectSize>,
|
||||||
default: 'large',
|
default: 'large',
|
||||||
validator: (value: string): boolean =>
|
|
||||||
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
|
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -101,7 +94,7 @@ export default defineComponent({
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
},
|
},
|
||||||
computedSize(): string | undefined {
|
computedSize(): InnerSelectRef['$props']['size'] {
|
||||||
if (this.size === 'medium') {
|
if (this.size === 'medium') {
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,150 +57,120 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import N8nInput from '../N8nInput';
|
import N8nInput from '../N8nInput';
|
||||||
import N8nMarkdown from '../N8nMarkdown';
|
import N8nMarkdown from '../N8nMarkdown';
|
||||||
import N8nResizeWrapper from '../N8nResizeWrapper';
|
import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
|
||||||
import N8nText from '../N8nText';
|
import N8nText from '../N8nText';
|
||||||
import Locale from '../../mixins/locale';
|
import { useI18n } from '../../composables/useI18n';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
interface StickyProps {
|
||||||
name: 'N8nSticky',
|
modelValue?: string;
|
||||||
components: {
|
height?: number;
|
||||||
N8nInput,
|
width?: number;
|
||||||
N8nMarkdown,
|
minHeight?: number;
|
||||||
N8nResizeWrapper,
|
minWidth?: number;
|
||||||
N8nText,
|
scale?: number;
|
||||||
},
|
gridSize?: number;
|
||||||
mixins: [Locale],
|
id?: string;
|
||||||
props: {
|
defaultText?: string;
|
||||||
modelValue: {
|
editMode?: boolean;
|
||||||
type: String,
|
readOnly?: boolean;
|
||||||
},
|
backgroundColor?: number | string;
|
||||||
height: {
|
}
|
||||||
type: Number,
|
|
||||||
default: 180,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
default: 240,
|
|
||||||
},
|
|
||||||
minHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 80,
|
|
||||||
},
|
|
||||||
minWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 150,
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
gridSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 20,
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
default: '0',
|
|
||||||
},
|
|
||||||
defaultText: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
editMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
readOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
|
||||||
value: [Number, String],
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isResizing: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
resHeight(): number {
|
|
||||||
if (this.height < this.minHeight) {
|
|
||||||
return this.minHeight;
|
|
||||||
}
|
|
||||||
return this.height;
|
|
||||||
},
|
|
||||||
resWidth(): number {
|
|
||||||
if (this.width < this.minWidth) {
|
|
||||||
return this.minWidth;
|
|
||||||
}
|
|
||||||
return this.width;
|
|
||||||
},
|
|
||||||
styles(): { height: string; width: string } {
|
|
||||||
const styles: { height: string; width: string } = {
|
|
||||||
height: `${this.resHeight}px`,
|
|
||||||
width: `${this.resWidth}px`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return styles;
|
const props = withDefaults(defineProps<StickyProps>(), {
|
||||||
},
|
height: 180,
|
||||||
shouldShowFooter(): boolean {
|
width: 240,
|
||||||
return this.resHeight > 100 && this.resWidth > 155;
|
minHeight: 80,
|
||||||
},
|
minWidth: 150,
|
||||||
},
|
scale: 1,
|
||||||
watch: {
|
gridSize: 20,
|
||||||
editMode(newMode, prevMode) {
|
id: '0',
|
||||||
setTimeout(() => {
|
editMode: false,
|
||||||
if (newMode && !prevMode && this.$refs.input) {
|
readOnly: false,
|
||||||
const textarea = this.$refs.input as HTMLTextAreaElement;
|
backgroundColor: 1,
|
||||||
if (this.defaultText === this.modelValue) {
|
|
||||||
textarea.select();
|
|
||||||
}
|
|
||||||
textarea.focus();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onDoubleClick() {
|
|
||||||
if (!this.readOnly) {
|
|
||||||
this.$emit('edit', true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onInputBlur() {
|
|
||||||
if (!this.isResizing) {
|
|
||||||
this.$emit('edit', false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdateModelValue(value: string) {
|
|
||||||
this.$emit('update:modelValue', value);
|
|
||||||
},
|
|
||||||
onMarkdownClick(link: string, event: Event) {
|
|
||||||
this.$emit('markdown-click', link, event);
|
|
||||||
},
|
|
||||||
onResize(values: unknown[]) {
|
|
||||||
this.$emit('resize', values);
|
|
||||||
},
|
|
||||||
onResizeEnd(resizeEnd: unknown) {
|
|
||||||
this.isResizing = false;
|
|
||||||
this.$emit('resizeend', resizeEnd);
|
|
||||||
},
|
|
||||||
onResizeStart() {
|
|
||||||
this.isResizing = true;
|
|
||||||
this.$emit('resizestart');
|
|
||||||
},
|
|
||||||
onInputScroll(event: WheelEvent) {
|
|
||||||
// Pass through zoom events but hold regular scrolling
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'edit', editing: boolean);
|
||||||
|
(event: 'update:modelValue', value: string);
|
||||||
|
(event: 'markdown-click', link: string, e: Event);
|
||||||
|
(event: 'resize', values: ResizeData);
|
||||||
|
(event: 'resizestart');
|
||||||
|
(event: 'resizeend', value: unknown);
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const isResizing = ref(false);
|
||||||
|
const input = ref<HTMLTextAreaElement | undefined>(undefined);
|
||||||
|
|
||||||
|
const resHeight = computed((): number => {
|
||||||
|
return props.height < props.minHeight ? props.minHeight : props.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resWidth = computed((): number => {
|
||||||
|
return props.width < props.minWidth ? props.minWidth : props.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = computed((): { height: string; width: string } => ({
|
||||||
|
height: `${resHeight.value}px`,
|
||||||
|
width: `${resWidth.value}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const shouldShowFooter = computed((): boolean => resHeight.value > 100 && resWidth.value > 155);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.editMode,
|
||||||
|
(newMode, prevMode) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (newMode && !prevMode && input.value) {
|
||||||
|
if (props.defaultText === props.modelValue) {
|
||||||
|
input.value.select();
|
||||||
|
}
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDoubleClick = () => {
|
||||||
|
if (!props.readOnly) $emit('edit', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputBlur = () => {
|
||||||
|
if (!isResizing.value) $emit('edit', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateModelValue = (value: string) => {
|
||||||
|
$emit('update:modelValue', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMarkdownClick = (link: string, event: Event) => {
|
||||||
|
$emit('markdown-click', link, event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = (values: ResizeData) => {
|
||||||
|
$emit('resize', values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeStart = () => {
|
||||||
|
isResizing.value = true;
|
||||||
|
$emit('resizestart');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeEnd = (resizeEnd: unknown) => {
|
||||||
|
isResizing.value = false;
|
||||||
|
$emit('resizeend', resizeEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputScroll = (event: WheelEvent) => {
|
||||||
|
// Pass through zoom events but hold regular scrolling
|
||||||
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -36,8 +36,12 @@ export const Example = Template.bind({});
|
||||||
Example.args = {
|
Example.args = {
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Test',
|
label: 'First',
|
||||||
value: 'test',
|
value: 'first',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Second',
|
||||||
|
value: 'second',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Github',
|
label: 'Github',
|
||||||
|
|
|
@ -47,12 +47,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
|
|
||||||
export interface N8nTabOptions {
|
interface TabOptions {
|
||||||
value: string;
|
value: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
@ -61,85 +60,63 @@ export interface N8nTabOptions {
|
||||||
align?: 'left' | 'right';
|
align?: 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
interface TabsProps {
|
||||||
name: 'N8nTabs',
|
modelValue?: string;
|
||||||
components: {
|
options?: TabOptions[];
|
||||||
N8nIcon,
|
}
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Array as PropType<N8nTabOptions[]>,
|
|
||||||
default: (): N8nTabOptions[] => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
scrollPosition: 0,
|
|
||||||
canScrollRight: false,
|
|
||||||
resizeObserver: null as ResizeObserver | null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const container = this.$refs.tabs as HTMLDivElement | undefined;
|
|
||||||
if (container) {
|
|
||||||
container.addEventListener('scroll', (event: Event) => {
|
|
||||||
const width = container.clientWidth;
|
|
||||||
const scrollWidth = container.scrollWidth;
|
|
||||||
this.scrollPosition = (event.target as Element).scrollLeft;
|
|
||||||
|
|
||||||
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
withDefaults(defineProps<TabsProps>(), {
|
||||||
});
|
options: () => [],
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
const width = container.clientWidth;
|
|
||||||
const scrollWidth = container.scrollWidth;
|
|
||||||
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(container);
|
|
||||||
|
|
||||||
const width = container.clientWidth;
|
|
||||||
const scrollWidth = container.scrollWidth;
|
|
||||||
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
if (this.resizeObserver) {
|
|
||||||
this.resizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleTooltipClick(tab: string, event: MouseEvent) {
|
|
||||||
this.$emit('tooltipClick', tab, event);
|
|
||||||
},
|
|
||||||
handleTabClick(tab: string) {
|
|
||||||
this.$emit('update:modelValue', tab);
|
|
||||||
},
|
|
||||||
scrollLeft() {
|
|
||||||
this.scroll(-50);
|
|
||||||
},
|
|
||||||
scrollRight() {
|
|
||||||
this.scroll(50);
|
|
||||||
},
|
|
||||||
scroll(left: number) {
|
|
||||||
const container = this.$refs.tabs as
|
|
||||||
| (HTMLDivElement & { scrollBy: ScrollByFunction })
|
|
||||||
| undefined;
|
|
||||||
if (container) {
|
|
||||||
container.scrollBy({ left, top: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScrollByFunction = (arg: {
|
const scrollPosition = ref(0);
|
||||||
left: number;
|
const canScrollRight = ref(false);
|
||||||
top: number;
|
const tabs = ref<Element | undefined>(undefined);
|
||||||
behavior: 'smooth' | 'instant' | 'auto';
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
}) => void;
|
|
||||||
|
onMounted(() => {
|
||||||
|
const container = tabs.value as Element;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('scroll', (event: Event) => {
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
scrollPosition.value = (event.target as Element).scrollLeft;
|
||||||
|
canScrollRight.value = scrollWidth - width > scrollPosition.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
canScrollRight.value = scrollWidth - width > scrollPosition.value;
|
||||||
|
});
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const scrollWidth = container.scrollWidth;
|
||||||
|
canScrollRight.value = scrollWidth - width > scrollPosition.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
const $emit = defineEmits<{
|
||||||
|
(event: 'tooltipClick', tab: string, e: MouseEvent): void;
|
||||||
|
(event: 'update:modelValue', tab: string);
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleTooltipClick = (tab: string, event: MouseEvent) => $emit('tooltipClick', tab, event);
|
||||||
|
const handleTabClick = (tab: string) => $emit('update:modelValue', tab);
|
||||||
|
|
||||||
|
const scroll = (left: number) => {
|
||||||
|
const container = tabs.value;
|
||||||
|
if (container) {
|
||||||
|
container.scrollBy({ left, top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scrollLeft = () => scroll(-50);
|
||||||
|
const scrollRight = () => scroll(50);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import N8nTag from '../N8nTag';
|
import N8nTag from '../N8nTag';
|
||||||
import N8nLink from '../N8nLink';
|
import N8nLink from '../N8nLink';
|
||||||
import { useI18n } from '../../composables/useI18n';
|
import { useI18n } from '../../composables/useI18n';
|
||||||
|
@ -46,11 +46,11 @@ const $emit = defineEmits(['expand', 'click:tag']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
let showAll = false;
|
const showAll = ref(false);
|
||||||
|
|
||||||
const visibleTags = computed((): ITag[] => {
|
const visibleTags = computed((): ITag[] => {
|
||||||
const { tags, truncate, truncateAt } = props;
|
const { tags, truncate, truncateAt } = props;
|
||||||
if (truncate && !showAll && tags.length > truncateAt) {
|
if (truncate && !showAll.value && tags.length > truncateAt) {
|
||||||
return tags.slice(0, truncateAt);
|
return tags.slice(0, truncateAt);
|
||||||
}
|
}
|
||||||
return tags;
|
return tags;
|
||||||
|
@ -59,7 +59,7 @@ const visibleTags = computed((): ITag[] => {
|
||||||
const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt);
|
const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt);
|
||||||
|
|
||||||
const onExpand = () => {
|
const onExpand = () => {
|
||||||
showAll = true;
|
showAll.value = true;
|
||||||
$emit('expand', true);
|
$emit('expand', true);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
:value="value[label]"
|
:value="value[label]"
|
||||||
:node-class="nodeClass"
|
:node-class="nodeClass"
|
||||||
>
|
>
|
||||||
<template v-for="(index, name) in $slots" #[name]="data">
|
<template v-for="(_, name) in $slots" #[name]="data">
|
||||||
<slot :name="name" v-bind="data"></slot>
|
<slot :name="name" v-bind="data"></slot>
|
||||||
</template>
|
</template>
|
||||||
</n8n-tree>
|
</n8n-tree>
|
||||||
|
@ -26,60 +26,51 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
interface TreeProps {
|
||||||
name: 'N8nTree',
|
value?: Record<string, unknown>;
|
||||||
components: {},
|
path?: string[];
|
||||||
props: {
|
depth?: number;
|
||||||
value: {
|
nodeClass?: string;
|
||||||
type: Object as PropType<Record<string, unknown>>,
|
}
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
depth: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
nodeClass: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
classes(): Record<string, boolean> {
|
|
||||||
return { [this.nodeClass]: !!this.nodeClass, [this.$style.indent]: this.depth > 0 };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isSimple(data: unknown): boolean {
|
|
||||||
if (data === null || data === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data === 'object' && Object.keys(data).length === 0) {
|
defineOptions({ name: 'N8nTree' });
|
||||||
return true;
|
const props = withDefaults(defineProps<TreeProps>(), {
|
||||||
}
|
value: () => ({}),
|
||||||
|
path: () => [],
|
||||||
if (Array.isArray(data) && data.length === 0) {
|
depth: 0,
|
||||||
return true;
|
nodeClass: '',
|
||||||
}
|
|
||||||
|
|
||||||
return typeof data !== 'object';
|
|
||||||
},
|
|
||||||
getPath(key: string): unknown[] {
|
|
||||||
if (Array.isArray(this.value)) {
|
|
||||||
return [...this.path, parseInt(key, 10)];
|
|
||||||
}
|
|
||||||
return [...this.path, key];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $style = useCssModule();
|
||||||
|
const classes = computed((): Record<string, boolean> => {
|
||||||
|
return { [props.nodeClass]: !!props.nodeClass, [$style.indent]: props.depth > 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSimple = (data: unknown): boolean => {
|
||||||
|
if (data === null || data === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'object' && Object.keys(data).length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data) && data.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof data !== 'object';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPath = (key: string): unknown[] => {
|
||||||
|
if (Array.isArray(props.value)) {
|
||||||
|
return [...props.path, parseInt(key, 10)];
|
||||||
|
}
|
||||||
|
return [...props.path, key];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
:filterable="true"
|
:filterable="true"
|
||||||
:filter-method="setFilter"
|
:filter-method="setFilter"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder || t('nds.userSelect.selectUser')"
|
||||||
:default-first-option="true"
|
:default-first-option="true"
|
||||||
teleported
|
teleported
|
||||||
:popper-class="$style.limitPopperWidth"
|
:popper-class="$style.limitPopperWidth"
|
||||||
|
@ -30,114 +30,83 @@
|
||||||
</N8nSelect>
|
</N8nSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import N8nUserInfo from '../N8nUserInfo';
|
import N8nUserInfo from '../N8nUserInfo';
|
||||||
import N8nSelect from '../N8nSelect';
|
import N8nSelect from '../N8nSelect';
|
||||||
import N8nOption from '../N8nOption';
|
import N8nOption from '../N8nOption';
|
||||||
import type { IUser } from '../../types';
|
import { useI18n } from '../../composables/useI18n';
|
||||||
import Locale from '../../mixins/locale';
|
import type { IUser, SelectSize } from '../../types';
|
||||||
import { t } from '../../locale';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
interface UserSelectProps {
|
||||||
name: 'N8nUserSelect',
|
users?: IUser[];
|
||||||
components: {
|
modelValue?: string;
|
||||||
N8nUserInfo,
|
ignoreIds?: string[];
|
||||||
N8nSelect,
|
currentUserId?: string;
|
||||||
N8nOption,
|
placeholder?: string;
|
||||||
},
|
size?: Exclude<SelectSize, 'xlarge'>;
|
||||||
mixins: [Locale],
|
}
|
||||||
props: {
|
|
||||||
users: {
|
|
||||||
type: Array as PropType<IUser[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
ignoreIds: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => [],
|
|
||||||
validator: (ids: string[]) => !ids.find((id) => typeof id !== 'string'),
|
|
||||||
},
|
|
||||||
currentUserId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: () => t('nds.userSelect.selectUser'),
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
validator: (value: string): boolean => ['mini', 'small', 'medium', 'large'].includes(value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filter: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filteredUsers(): IUser[] {
|
|
||||||
return this.users.filter((user) => {
|
|
||||||
if (user.isPendingUser || !user.email) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ignoreIds.includes(user.id)) {
|
const props = withDefaults(defineProps<UserSelectProps>(), {
|
||||||
return false;
|
users: () => [],
|
||||||
}
|
modelValue: '',
|
||||||
|
ignoreIds: () => [],
|
||||||
if (user.fullName) {
|
currentUserId: '',
|
||||||
const match = user.fullName.toLowerCase().includes(this.filter.toLowerCase());
|
|
||||||
if (match) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.email.includes(this.filter);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sortedUsers(): IUser[] {
|
|
||||||
return [...this.filteredUsers].sort((a: IUser, b: IUser) => {
|
|
||||||
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
|
|
||||||
return a.lastName > b.lastName ? 1 : -1;
|
|
||||||
}
|
|
||||||
if (a.firstName && b.firstName && a.firstName !== b.firstName) {
|
|
||||||
return a.firstName > b.firstName ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!a.email || !b.email) {
|
|
||||||
throw new Error('Expected all users to have email');
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.email > b.email ? 1 : -1;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setFilter(value: string) {
|
|
||||||
this.filter = value;
|
|
||||||
},
|
|
||||||
onBlur() {
|
|
||||||
this.$emit('blur');
|
|
||||||
},
|
|
||||||
onFocus() {
|
|
||||||
this.$emit('focus');
|
|
||||||
},
|
|
||||||
getLabel(user: IUser) {
|
|
||||||
if (!user.fullName) {
|
|
||||||
return user.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${user.fullName} (${user.email})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $emit = defineEmits(['blur', 'focus']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const filter = ref('');
|
||||||
|
|
||||||
|
const filteredUsers = computed(() =>
|
||||||
|
props.users.filter((user) => {
|
||||||
|
if (user.isPendingUser || !user.email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.ignoreIds.includes(user.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.fullName) {
|
||||||
|
const match = user.fullName.toLowerCase().includes(filter.value.toLowerCase());
|
||||||
|
if (match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.email.includes(filter.value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedUsers = computed(() =>
|
||||||
|
[...filteredUsers.value].sort((a: IUser, b: IUser) => {
|
||||||
|
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
|
||||||
|
return a.lastName > b.lastName ? 1 : -1;
|
||||||
|
}
|
||||||
|
if (a.firstName && b.firstName && a.firstName !== b.firstName) {
|
||||||
|
return a.firstName > b.firstName ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!a.email || !b.email) {
|
||||||
|
throw new Error('Expected all users to have email');
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.email > b.email ? 1 : -1;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setFilter = (value: string) => {
|
||||||
|
filter.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = () => $emit('blur');
|
||||||
|
const onFocus = () => $emit('focus');
|
||||||
|
|
||||||
|
const getLabel = (user: IUser) =>
|
||||||
|
!user.fullName ? user.email : `${user.fullName} (${user.email})`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -23,8 +23,8 @@ export default function () {
|
||||||
args = {} as unknown as Array<string | object>;
|
args = {} as unknown as Array<string | object>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.replace(RE_NARGS, (match, prefix, i, index: number) => {
|
return str.replace(RE_NARGS, (match, _, i, index: number) => {
|
||||||
let result;
|
let result: string | object | null;
|
||||||
|
|
||||||
if (str[index - 1] === '{' && str[index + match.length] === '}') {
|
if (str[index - 1] === '{' && str[index + match.length] === '}') {
|
||||||
return i;
|
return i;
|
||||||
|
|
|
@ -15,61 +15,52 @@
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue';
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
interface VariableTableProps {
|
||||||
name: 'VariableTable',
|
variables: string[];
|
||||||
props: {
|
attr?: string;
|
||||||
variables: {
|
}
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
attr: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
observer: null as null | MutationObserver,
|
|
||||||
values: {} as Record<string, string>,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const setValues = () => {
|
|
||||||
this.variables.forEach((variable) => {
|
|
||||||
const style = getComputedStyle(document.body);
|
|
||||||
const value = style.getPropertyValue(variable);
|
|
||||||
|
|
||||||
this.values = {
|
const props = withDefaults(defineProps<VariableTableProps>(), {
|
||||||
...this.values,
|
attr: '',
|
||||||
[variable]: value,
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setValues();
|
let observer: MutationObserver | null = null;
|
||||||
|
let values: Record<string, string> = {};
|
||||||
|
|
||||||
// when theme class is added or removed, reset color values
|
onMounted(() => {
|
||||||
this.observer = new MutationObserver((mutationsList) => {
|
const setValues = () => {
|
||||||
for (const mutation of mutationsList) {
|
props.variables.forEach((variable) => {
|
||||||
if (mutation.type === 'attributes') {
|
const style = getComputedStyle(document.body);
|
||||||
setValues();
|
const value = style.getPropertyValue(variable);
|
||||||
}
|
|
||||||
}
|
values = {
|
||||||
|
...values,
|
||||||
|
[variable]: value,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const body = document.querySelector('body');
|
};
|
||||||
if (body) {
|
|
||||||
this.observer.observe(body, { attributes: true });
|
setValues();
|
||||||
|
|
||||||
|
// when theme class is added or removed, reset color values
|
||||||
|
observer = new MutationObserver((mutationsList) => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
if (mutation.type === 'attributes') {
|
||||||
|
setValues();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
unmounted() {
|
const body = document.querySelector('body');
|
||||||
if (this.observer) {
|
if (body) {
|
||||||
this.observer.disconnect();
|
observer.observe(body, { attributes: true });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
observer?.disconnect();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,42 +9,37 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
interface SpacingPreviewProps {
|
||||||
name: 'SpacingPreview',
|
property?: 'padding' | 'margin';
|
||||||
props: {
|
side?: string;
|
||||||
property: {
|
}
|
||||||
type: String,
|
|
||||||
default: 'padding',
|
const SIZES = [
|
||||||
},
|
'0',
|
||||||
side: {
|
'5xs',
|
||||||
type: String,
|
'4xs',
|
||||||
default: '',
|
'3xs',
|
||||||
},
|
'2xs',
|
||||||
},
|
'xs',
|
||||||
computed: {
|
's',
|
||||||
sizes() {
|
'm',
|
||||||
return [
|
'l',
|
||||||
'0',
|
'xl',
|
||||||
'5xs',
|
'2xl',
|
||||||
'4xs',
|
'3xl',
|
||||||
'3xs',
|
'4xl',
|
||||||
'2xs',
|
'5xl',
|
||||||
'xs',
|
] as const;
|
||||||
's',
|
|
||||||
'm',
|
const props = withDefaults(defineProps<SpacingPreviewProps>(), {
|
||||||
'l',
|
property: 'padding',
|
||||||
'xl',
|
side: '',
|
||||||
'2xl',
|
|
||||||
'3xl',
|
|
||||||
'4xl',
|
|
||||||
'5xl',
|
|
||||||
].concat(this.property === 'margin' ? ['auto'] : []);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -4,7 +4,7 @@ export default {
|
||||||
title: 'Utilities/Lists',
|
title: 'Utilities/Lists',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({
|
const ListStyleNoneTemplate: StoryFn = (_, { argTypes }) => ({
|
||||||
props: Object.keys(argTypes),
|
props: Object.keys(argTypes),
|
||||||
template: `<ul class="list-style-none">
|
template: `<ul class="list-style-none">
|
||||||
<li>List item 1</li>
|
<li>List item 1</li>
|
||||||
|
@ -15,7 +15,7 @@ const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({
|
||||||
|
|
||||||
export const StyleNone = ListStyleNoneTemplate.bind({});
|
export const StyleNone = ListStyleNoneTemplate.bind({});
|
||||||
|
|
||||||
const ListInlineTemplate: StoryFn = (args, { argTypes }) => ({
|
const ListInlineTemplate: StoryFn = (_, { argTypes }) => ({
|
||||||
props: Object.keys(argTypes),
|
props: Object.keys(argTypes),
|
||||||
template: `<ul class="list-inline">
|
template: `<ul class="list-inline">
|
||||||
<li>List item 1</li>
|
<li>List item 1</li>
|
||||||
|
|
|
@ -3,6 +3,6 @@ export * from './datatable';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
export * from './i18n';
|
export * from './i18n';
|
||||||
export * from './menu';
|
export * from './menu';
|
||||||
export * from './router';
|
export * from './select';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './keyboardshortcut';
|
export * from './keyboardshortcut';
|
||||||
|
|
|
@ -8,7 +8,7 @@ export type IMenuItem = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
secondaryIcon?: {
|
secondaryIcon?: {
|
||||||
name: string;
|
name: string;
|
||||||
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
|
size?: 'xsmall' | 'small' | 'medium' | 'large';
|
||||||
tooltip?: Partial<ElTooltipProps>;
|
tooltip?: Partial<ElTooltipProps>;
|
||||||
};
|
};
|
||||||
customIconSize?: 'medium' | 'small';
|
customIconSize?: 'medium' | 'small';
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface RouteObject {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
2
packages/design-system/src/types/select.ts
Normal file
2
packages/design-system/src/types/select.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
const SELECT_SIZES = ['mini', 'small', 'medium', 'large', 'xlarge'] as const;
|
||||||
|
export type SelectSize = (typeof SELECT_SIZES)[number];
|
|
@ -1,11 +1,5 @@
|
||||||
import { createEventBus } from '../event-bus';
|
import { createEventBus } from '../event-bus';
|
||||||
|
|
||||||
// @TODO: Remove when conflicting vitest and jest globals are reconciled
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
||||||
const vi: (typeof import('vitest'))['vitest'];
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('createEventBus()', () => {
|
describe('createEventBus()', () => {
|
||||||
const eventBus = createEventBus();
|
const eventBus = createEventBus();
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
// TODO: remove all options below this line
|
// TODO: remove all options below this line
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noImplicitReturns": false
|
"noImplicitReturns": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||||
|
|
Loading…
Reference in a new issue