Add content element action menu

This commit is contained in:
Ramon Wenger 2022-02-15 17:21:41 +01:00
parent bde635b21c
commit 13a5ea9534
15 changed files with 415 additions and 82 deletions

View File

@ -9,8 +9,6 @@
import PlusIcon from '@/components/icons/PlusIcon'; import PlusIcon from '@/components/icons/PlusIcon';
export default { export default {
components: { PlusIcon } components: { PlusIcon }
//
}; };
</script> </script>
@ -19,8 +17,10 @@
$color: $color-silver-dark; $color: $color-silver-dark;
.add-content-link { .add-content-link {
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: flex-start;
cursor: pointer;
&__icon { &__icon {
width: 20px; width: 20px;

View File

@ -29,7 +29,6 @@
<!-- Add content at top of content block --> <!-- Add content at top of content block -->
<add-content-link <add-content-link
class="content-block-form__segment"
@click="addBlock(-1)" @click="addBlock(-1)"
/> />
<!-- Loop for outer contents layer --> <!-- Loop for outer contents layer -->
@ -39,31 +38,45 @@
:key="block.id" :key="block.id"
> >
<!-- If the block is a content list --> <!-- If the block is a content list -->
<ol <div
class="content-list__item content-block-form__segment" class="content-block-form__segment"
data-cy="content-list-item"
v-if="block.type === 'content_list_item'" v-if="block.type === 'content_list_item'"
> >
<li <content-element-actions
class="content-block-form__segment" class="content-block-form__actions"
v-for="(content, index) in block.contents" @remove="remove(outer)"
:key="content.id" @move-up="up(outer)"
@move-down="down(outer)"
@move-top="top(outer)"
@move-bottom="bottom(outer)"
/>
<ol
class="content-list__item"
data-cy="content-list-item"
> >
<content-element <li
:element="content"
class="content-block-form__segment" class="content-block-form__segment"
@update="update(index, $event, outer)" v-for="(content, index) in block.contents"
@remove="remove(outer, index)" :key="content.id"
@up="up(outer, index)" >
@down="down(outer, index)" <content-element
/> :element="content"
class="content-block-form__segment"
@update="update(index, $event, outer)"
@remove="remove(outer, index)"
@up="up(outer, index)"
@down="down(outer, index)"
@top="top(outer, index)"
@bottom="bottom(outer, index)"
/>
<add-content-link <add-content-link
class="content-block-form__add-button" class="content-block-form__add-button"
@click="addBlock(outer, index)" @click="addBlock(outer, index)"
/> />
</li> </li>
</ol> </ol>
</div>
<!-- If the block is a single element --> <!-- If the block is a single element -->
<content-element <content-element
:element="block" :element="block"
@ -73,11 +86,12 @@
@remove="remove(outer)" @remove="remove(outer)"
@up="up(outer)" @up="up(outer)"
@down="down(outer)" @down="down(outer)"
@top="top(outer)"
@bottom="bottom(outer)"
/> />
<!-- Add element after the looped item --> <!-- Add element after the looped item -->
<add-content-link <add-content-link
class="content-block-form__segment"
@click="addBlock(outer)" @click="addBlock(outer)"
/> />
</div> </div>
@ -107,23 +121,25 @@
import AddContentLink from '@/components/content-block-form/AddContentLink'; import AddContentLink from '@/components/content-block-form/AddContentLink';
import ContentElement from '@/components/content-block-form/ContentElement'; import ContentElement from '@/components/content-block-form/ContentElement';
import {removeAtIndex, replaceAtIndex, swapElements} from '@/graphql/immutable-operations'; import {moveToIndex, removeAtIndex, replaceAtIndex, swapElements} from '@/graphql/immutable-operations';
import {CHOOSER, transformInnerContents} from '@/components/content-block-form/helpers'; import {CHOOSER, transformInnerContents} from '@/components/content-block-form/helpers';
import ContentElementActions from '@/components/content-block-form/ContentElementActions';
export default { export default {
props: { props: {
title: { title: {
type: String, type: String,
default: '' default: '',
}, },
contentBlock: { contentBlock: {
type: Object, type: Object,
required: true required: true,
} },
}, },
components: { components: {
ContentElementActions,
ContentElement, ContentElement,
AddContentLink, AddContentLink,
InputWithLabel, InputWithLabel,
@ -144,7 +160,7 @@
computed: { computed: {
isValid() { isValid() {
return this.localContentBlock.title > ''; return this.localContentBlock.title > '';
} },
}, },
methods: { methods: {
update(index, element, parent) { update(index, element, parent) {
@ -153,7 +169,7 @@
this.localContentBlock.contents = [ this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, index), ...this.localContentBlock.contents.slice(0, index),
element, element,
...this.localContentBlock.contents.slice(index + 1) ...this.localContentBlock.contents.slice(index + 1),
]; ];
} else { } else {
const parentBlock = this.localContentBlock.contents[parent]; const parentBlock = this.localContentBlock.contents[parent];
@ -166,9 +182,9 @@
...parentBlock.contents.slice(0, index), ...parentBlock.contents.slice(0, index),
element, element,
...parentBlock.contents.slice(index + 1), ...parentBlock.contents.slice(index + 1),
] ],
}, },
...this.localContentBlock.contents.slice(parent + 1) ...this.localContentBlock.contents.slice(parent + 1),
]; ];
} }
}, },
@ -185,20 +201,20 @@
id: -1, id: -1,
type: CHOOSER, type: CHOOSER,
}, },
...block.contents.slice(innerIndex + 1) ...block.contents.slice(innerIndex + 1),
] ],
}, },
...this.localContentBlock.contents.slice(afterOuterIndex + 1) ...this.localContentBlock.contents.slice(afterOuterIndex + 1),
]; ];
} else { } else {
this.localContentBlock.contents = [ this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, afterOuterIndex+1), ...this.localContentBlock.contents.slice(0, afterOuterIndex + 1),
{ {
id: -1, id: -1,
type: CHOOSER, type: CHOOSER,
includeListOption: true includeListOption: true,
}, },
...this.localContentBlock.contents.slice(afterOuterIndex+1) ...this.localContentBlock.contents.slice(afterOuterIndex + 1),
]; ];
} }
}, },
@ -219,17 +235,45 @@
const outerElement = contents[outer]; const outerElement = contents[outer];
const newOuterElement = { const newOuterElement = {
...outerElement, ...outerElement,
contents: swapElements(outerElement.contents, inner, inner + distance) contents: swapElements(outerElement.contents, inner, inner + distance),
}; };
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement); this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
} }
}, },
up(outer, inner){ top(outer, inner) {
if (inner === undefined) {
this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, 0);
} else {
const {contents} = this.localContentBlock;
const outerElement = contents[outer];
const newOuterElement = {
...outerElement,
contents: moveToIndex(outerElement.contents, inner, 0),
};
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
}
},
up(outer, inner) {
this.shift(outer, inner, -1); this.shift(outer, inner, -1);
}, },
down(outer, inner){ down(outer, inner) {
this.shift(outer, inner, 1); this.shift(outer, inner, 1);
}, },
bottom(outer, inner) {
if (inner === undefined) {
const maxIndex = this.localContentBlock.contents.length - 1;
this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, maxIndex);
} else {
const {contents} = this.localContentBlock;
const outerElement = contents[outer];
const maxIndex = outerElement.contents.length - 1;
const newOuterElement = {
...outerElement,
contents: moveToIndex(outerElement.contents, inner, maxIndex),
};
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
}
},
executeRemoval(outer, inner) { executeRemoval(outer, inner) {
if (inner === undefined) { if (inner === undefined) {
// not a list item container, just remove the element from the outer array // not a list item container, just remove the element from the outer array
@ -245,7 +289,7 @@
*/ */
let element = { let element = {
...this.localContentBlock.contents[outer], ...this.localContentBlock.contents[outer],
contents: innerContents contents: innerContents,
}; };
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, outer, element); this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, outer, element);
} else { } else {
@ -257,7 +301,7 @@
save(contentBlock) { save(contentBlock) {
this.$emit('save', contentBlock); this.$emit('save', contentBlock);
}, },
} },
}; };
</script> </script>
@ -287,12 +331,21 @@
} }
&__segment { &__segment {
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
:last-child { :last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
&__actions {
align-self: flex-end;
}
&__content { &__content {
grid-area: content; grid-area: content;
overflow-x: visible; overflow-x: visible;

View File

@ -1,18 +1,26 @@
<template> <template>
<div class="content-element"> <div class="content-element">
<!-- Element Chooser if element has chooser type or no type -->
<content-block-element-chooser-widget <content-block-element-chooser-widget
:class="['content-element__component', 'content-element__chooser']" :class="['content-element__component', 'content-element__chooser']"
v-bind="element" v-bind="element"
v-if="isChooser" v-if="isChooser"
@change-type="changeType" @change-type="changeType"
/> />
<!-- Content Forms -->
<content-form-section <content-form-section
:title="title" :title="title"
:has-actions="true"
:icon="icon" :icon="icon"
v-else v-else
@top="$emit('top')"
@up="$emit('up')"
@down="$emit('down')"
@bottom="$emit('bottom')"
@remove="$emit('remove')"
> >
<div class="content-element__section"> <div class="content-element__section">
<!-- Form depending on type of element -->
<component <component
:class="['content-element__component']" :class="['content-element__component']"
v-bind="element" v-bind="element"
@ -28,27 +36,6 @@
@assignment-change-title="changeAssignmentTitle" @assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment" @assignment-change-assignment="changeAssignmentAssignment"
/> />
<a
class="contents-form__remove icon-button"
@click="$emit('remove')"
>
<trash-icon
class="contents-form__trash-icon icon-button__icon"
/>
</a>
<!-- <a-->
<!-- class="button"-->
<!-- @click="$emit('up')"-->
<!-- >-->
<!-- Up-->
<!-- </a>-->
<!-- <a-->
<!-- class="button"-->
<!-- @click="$emit('down')"-->
<!-- >-->
<!-- Down-->
<!-- </a>-->
</div> </div>
</content-form-section> </content-form-section>
</div> </div>
@ -56,6 +43,7 @@
<script> <script>
import ContentFormSection from '@/components/content-block-form/ContentFormSection'; import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import ContentElementActions from '@/components/content-block-form/ContentElementActions';
const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'); const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon');
const ContentBlockElementChooserWidget = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget'); const ContentBlockElementChooserWidget = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget');
const LinkForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm'); const LinkForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm');
@ -77,6 +65,7 @@
}, },
components: { components: {
ContentElementActions,
ContentFormSection, ContentFormSection,
TrashIcon, TrashIcon,
ContentBlockElementChooserWidget, ContentBlockElementChooserWidget,
@ -170,6 +159,9 @@
}, },
}); });
}, },
debug(arg) {
console.log(arg);
},
changeUrl(value) { changeUrl(value) {
this._updateProperty(value, 'url'); this._updateProperty(value, 'url');
}, },
@ -261,9 +253,18 @@
@import '~styles/helpers'; @import '~styles/helpers';
.content-element { .content-element {
display: flex;
flex-direction: column;
&__actions {
display: inline-flex;
justify-self: flex-end;
align-self: flex-end;
}
&__section { &__section {
display: grid; display: grid;
grid-template-columns: 1fr 50px; //grid-template-columns: 1fr 50px;
grid-auto-rows: auto; grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/ /*width: 95%; // reserve space for scrollbar*/
} }

View File

@ -0,0 +1,129 @@
<template>
<div class="content-element-actions">
<button
class="icon-button"
@click.stop="toggle(true)"
>
<ellipses class="icon-button__icon" />
</button>
<widget-popover
class="content-element-actions__popover"
:no-padding="true"
v-if="show"
@hide-me="toggle(false)"
>
<section class="content-element-actions__section">
<button-with-icon-and-text
class="content-element-actions__button"
:large="true"
icon="arrow-thin-top"
text="Ganz nach oben verschieben"
@click="emitAndClose('move-top')"
/>
<button-with-icon-and-text
class="content-element-actions__button"
:large="true"
icon="arrow-thin-up"
text="Nach oben verschieben"
@click="emitAndClose('move-up')"
/>
<button-with-icon-and-text
class="content-element-actions__button"
:large="true"
icon="arrow-thin-down"
text="Nach unten verschieben"
@click="emitAndClose('move-down')"
/>
<button-with-icon-and-text
class="content-element-actions__button"
:large="true"
icon="arrow-thin-bottom"
text="Ganz nach unten verschieben"
@click="emitAndClose('move-bottom')"
/>
</section>
<section class="content-element-actions__section">
<button-with-icon-and-text
class="content-element-actions__button"
:large="true"
icon="trash-icon"
text="Löschen"
@click="emitAndClose('remove')"
/>
</section>
</widget-popover>
</div>
</template>
<script>
import WidgetPopover from '@/components/ui/WidgetPopover';
import Ellipses from '@/components/icons/Ellipses';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
export default {
components: {ButtonWithIconAndText, Ellipses, WidgetPopover},
data: () => ({
show: false,
}),
methods: {
toggle(show) {
this.show = show;
},
close() {
this.show = false;
},
up() {
this.$emit('move-up');
this.close();
},
down() {
this.$emit('move-down');
this.close();
},
top() {
this.$emit('move-top');
this.close();
},
bottom() {
this.$emit('move-bottom');
this.close();
},
emitAndClose(event) {
this.$emit(event);
this.close();
}
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.content-element-actions {
position: relative;
&__popover {
white-space: nowrap;
top: 100%;
transform: translateY($small-spacing);
}
&__section {
border-bottom: 1px solid $color-silver-dark;
padding: $medium-spacing $small-spacing;
&:last-child {
border-bottom: 0;
}
}
&__button {
margin-bottom: $medium-spacing;
}
}
</style>

View File

@ -6,12 +6,25 @@
:is="icon" :is="icon"
/> <span class="content-form-section__title">{{ title }}</span> /> <span class="content-form-section__title">{{ title }}</span>
</h2> </h2>
<slot />
<content-element-actions
class="content-form-section__actions"
v-if="hasActions"
@remove="$emit('remove')"
@move-top="$emit('top')"
@move-up="$emit('up')"
@move-down="$emit('down')"
@move-bottom="$emit('bottom')"
/>
<div class="content-form-section__content">
<slot />
</div>
</div> </div>
</template> </template>
<script> <script>
import formElementIcons from '@/components/ui/form-element-icons'; import formElementIcons from '@/components/ui/form-element-icons';
import ContentElementActions from '@/components/content-block-form/ContentElementActions';
export default { export default {
props: { props: {
@ -22,9 +35,14 @@
icon: { icon: {
type: String, type: String,
default: '' default: ''
},
hasActions: {
type: Boolean,
default: false
} }
}, },
components: { components: {
ContentElementActions,
...formElementIcons ...formElementIcons
} }
}; };
@ -39,9 +57,23 @@
padding: $small-spacing $medium-spacing; padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
grid-template-areas: 'h a' 'c c';
align-items: center;
grid-row-gap: $medium-spacing;
&__heading { &__heading {
display: flex; display: flex;
align-items: center; align-items: center;
grid-area: h;
margin-bottom: 0;
}
&__actions {
grid-area: a;
justify-self: end;
} }
&__title { &__title {
@ -54,5 +86,9 @@
height: 28px; height: 28px;
margin-right: $small-spacing; margin-right: $small-spacing;
} }
&__content {
grid-area: c;
}
} }
</style> </style>

View File

@ -0,0 +1,11 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
>
<path
d="M79 80.8H51.6l.1-.1 25.7-25.8c1-1 1-2.6 0-3.5-1-1-2.6-1-3.5 0L52.5 72.9V16.7c0-1.4-1.1-2.5-2.5-2.5s-2.5 1.1-2.5 2.5v56.2L26.1 51.4c-1-1-2.6-1-3.5 0-1 1-1 2.6 0 3.5l25.7 25.8.1.1H21c-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5h58c1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5z"
id="shape"
/>
</svg>
</template>

View File

@ -0,0 +1,12 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
xml:space="preserve"
>
<path
d="M77.5 55.7c-1-1-2.6-1-3.5 0L52.5 77.2V21.1c0-1.4-1.1-2.5-2.5-2.5s-2.5 1.1-2.5 2.5v56.2L26.1 55.8c-1-1-2.6-1-3.5 0-1 1-1 2.6 0 3.5l25.7 25.8c.1.1.2.2.4.3 0 0 .1 0 .1.1.1.1.2.1.3.2h.1c.1 0 .2.1.3.1h1c.1 0 .2-.1.3-.1h.1c.1 0 .2-.1.3-.2 0 0 .1 0 .1-.1.1-.1.3-.2.4-.3l25.7-25.8c1.1-1 1.1-2.6.1-3.6z"
id="arrow-thin-down"
/>
</svg>
</template>

View File

@ -0,0 +1,13 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
viewBox="0 0 100 100"
id="shape"
>
<path
d="M79,14.2H21a2.5,2.5,0,0,0,0,5H48.35l-.12.1h0L22.52,45.08a2.5,2.5,0,1,0,3.54,3.53L47.5,27.12V83.3a2.5,2.5,0,1,0,5,0V27.12L73.94,48.61a2.5,2.5,0,1,0,3.54-3.53L51.77,19.3h0l-.12-.1H79a2.5,2.5,0,0,0,0-5Z"
/>
</svg>
</template>

View File

@ -0,0 +1,11 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
xml:space="preserve"
>
<path
d="M77.5 45.1 51.8 19.3c-.5-.5-1.1-.7-1.8-.7s-1.3.3-1.8.7L22.5 45.1c-1 1-1 2.6 0 3.5 1 1 2.6 1 3.5 0l21.4-21.5v56.2c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5V27.1l21.4 21.5c.5.5 1.1.7 1.8.7.6 0 1.3-.2 1.8-.7 1.1-1 1.1-2.5.1-3.5z"
id="arrow-thin-up"
/></svg>
</template>

View File

@ -1,5 +1,8 @@
<template> <template>
<a class="button-with-icon-and-text"> <a
:class="[ 'button-with-icon-and-text', {'button-with-icon-and-text--large': large}]"
@click="$emit('click')"
>
<component <component
class="button-with-icon-and-text__icon" class="button-with-icon-and-text__icon"
:is="icon" :is="icon"
@ -9,8 +12,7 @@
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); import formElementIcons from '@/components/ui/form-element-icons';
const DocumentWithLinesIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentWithLinesIcon');
export default { export default {
props: { props: {
@ -21,12 +23,15 @@
text: { text: {
type: String, type: String,
default: '' default: ''
},
large: {
type: Boolean,
default: false
} }
}, },
components: { components: {
DocumentIcon, ...formElementIcons
DocumentWithLinesIcon
} }
}; };
</script> </script>
@ -48,5 +53,13 @@
&__text { &__text {
@include small-text; @include small-text;
} }
$p: &;
&--large {
#{$p}__text {
@include large-link;
}
}
} }
</style> </style>

View File

@ -4,7 +4,7 @@
class="widget-popover" class="widget-popover"
v-click-outside="hidePopover" v-click-outside="hidePopover"
> >
<ul class="widget-popover__links popover-links"> <ul :class="['widget-popover__links popover-links', {'popover-links--no-padding': noPadding}]">
<slot /> <slot />
</ul> </ul>
</div> </div>
@ -16,6 +16,10 @@
mobile: { mobile: {
type: Boolean, type: Boolean,
default: false default: false
},
noPadding: {
type: Boolean,
default: false
} }
}, },
@ -26,7 +30,3 @@
} }
}; };
</script> </script>
<style scoped lang="scss">
</style>

View File

@ -5,7 +5,19 @@ const TextIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons
const SpeechBubbleIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/SpeechBubbleIcon'); const SpeechBubbleIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/SpeechBubbleIcon');
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon');
const TitleIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TitleIcon'); const TitleIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TitleIcon');
const DocumentWithLinesIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentWithLinesIcon');
const ArrowThinBottom = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinBottom');
const ArrowThinDown = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinDown');
const ArrowThinTop = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinTop');
const ArrowThinUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinUp');
const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon');
/*
for icons with a single word, leave the *-icon name, to prevent conflicts
for others, both is fine: arrow-thin-up-icon or arrow-thin-up
*/
export default { export default {
LinkIcon, LinkIcon,
VideoIcon, VideoIcon,
@ -13,5 +25,11 @@ export default {
TextIcon, TextIcon,
SpeechBubbleIcon, SpeechBubbleIcon,
DocumentIcon, DocumentIcon,
TitleIcon TitleIcon,
DocumentWithLinesIcon,
ArrowThinBottom,
ArrowThinDown,
ArrowThinTop,
ArrowThinUp,
TrashIcon
}; };

View File

@ -42,3 +42,16 @@ export const swapElements = (arr, idx1, idx2)=>{
...arr.slice(larger+1) ...arr.slice(larger+1)
]; ];
}; };
export const moveToIndex = (arr, from, to) => {
const maxLength = arr.length - 1;
if (from < 0 || to < 0 || from > maxLength || to > maxLength) {
throw new Error('Index out of bounds');
}
const element = arr[from];
const newArr = [...arr.slice(0, from), ...arr.slice(from+1)];
return [
...newArr.slice(0, to),
element,
...newArr.slice(to)
];
};

View File

@ -24,6 +24,10 @@
flex-direction: column; flex-direction: column;
padding: $small-spacing; padding: $small-spacing;
&--no-padding {
padding: 0;
}
&__icon { &__icon {
width: 25px; width: 25px;
height: 25px; height: 25px;

View File

@ -1,4 +1,12 @@
import {removeAtIndex, replaceAtIndex, insertAtIndex, pushToArray, swapElements} from '@/graphql/immutable-operations'; import {
insertAtIndex,
moveToIndex,
pushToArray,
removeAtIndex,
replaceAtIndex,
swapElements,
} from '@/graphql/immutable-operations';
describe('Cache operations', () => { describe('Cache operations', () => {
it('removes at index', () => { it('removes at index', () => {
const original = [1, 2, 3]; const original = [1, 2, 3];
@ -75,14 +83,14 @@ describe('Cache operations', () => {
expect(copy).toEqual([3, 2, 1]); expect(copy).toEqual([3, 2, 1]);
}); });
it('swaps neighbors', ()=>{ it('swaps neighbors', () => {
const original = [1, 2, 3, 4, 5]; const original = [1, 2, 3, 4, 5];
const copy = swapElements(original, 3, 2); const copy = swapElements(original, 3, 2);
expect(copy).toEqual([1, 2, 4, 3, 5]); expect(copy).toEqual([1, 2, 4, 3, 5]);
}); });
it('does nothing with wrong indices', ()=>{ it('does nothing with wrong indices', () => {
const original = [1, 2, 3, 4, 5]; const original = [1, 2, 3, 4, 5];
const copy1 = swapElements(original, -1, 2); const copy1 = swapElements(original, -1, 2);
const copy2 = swapElements(original, 1, 99); const copy2 = swapElements(original, 1, 99);
@ -97,4 +105,15 @@ describe('Cache operations', () => {
expect(copy2.length).toBe(5); expect(copy2.length).toBe(5);
expect(copy3.length).toBe(5); expect(copy3.length).toBe(5);
}); });
it('moves to a specific index', () => {
const original = [1, 2, 3, 4, 5, 6, 7];
const toStart = moveToIndex(original, 3, 0);
const toEnd = moveToIndex(original, 3, original.length - 1);
const toMiddle = moveToIndex(original, 3, 2);
expect(toStart).toEqual([4, 1, 2, 3, 5, 6, 7]);
expect(toEnd).toEqual([1, 2, 3, 5, 6, 7, 4]);
expect(toMiddle).toEqual([1, 2, 4, 3, 5, 6, 7]);
});
}); });