Jump to content
Search Community

Recommended Posts

Posted

I'm trying to make the elements resizeble with drag, with vuejs but when placing the mouse inside the handler of each axis to reseize it is doing the drag and not the resize. I don't know what I should adjust, I'm wearing vuejs.

 

https://stackblitz.com/edit/gsap-reseble?file=app.vue

I want to do the same as the example here

 , but it doesn't work

 

Could you help me?

Posted

Hi @calango and welcome to the GSAP Forums!

 

Unfortunately there is far too much code in your demo and we don't have the time resources to comb through all that code trying to find what could be the issue, also I don't see any element that can be dragged in that demo. I'd strongly suggest to isolate this as much as possible and present just the element you want to drag/resize with the smallest amount of code. Is worth mentioning that this is not the simplest thing to achieve as shown in this demo:

See the Pen d2ac612530e18ee0c1921263c15cdbc7 by rhernando (@rhernando) on CodePen.

 

Of course that uses PIXI and is 8 years old, so probably a lot can be updated and improved, but this is not in any way the simplest thing to accomplish as I already mentioned.

 

Happy Tweening!

Posted
16 minutes ago, Rodrigo said:

Hi @calango and welcome to the GSAP Forums!

 

Infelizmente, há muito código em sua demonstração e não temos recursos de tempo para vasculhar todo esse código tentando encontrar qual poderia ser o problema, também não vejo nenhum elemento que possa ser arrastado nessa demonstração. Eu sugiro fortemente isolar isso o máximo possível e apresentar apenas o elemento que você deseja arrastar / redimensionar com a menor quantidade de código. Vale ressaltar que esta não é a coisa mais simples de conseguir, conforme mostrado nesta demonstração:

 

 

 

Claro que usa PIXI e tem 8 anos, então provavelmente muito pode ser atualizado e melhorado, mas isso não é de forma alguma a coisa mais simples de realizar, como já mencionei.

 

Feliz interpolação!

Hello Rodrigo, the only part of the code that makes draggable is APP.vue

I'm using gsap and I don't understand the change very well, but I need to apply this to both images and text. My example is a side menu for inserting images and text


I can't change the structure of draggable other than with

 

Draggable.create(el, {
    type: 'x,y',
    bounds: canvas.value,
    allowNativeTouchScrolling: true,
    liveSnap: false,


..... Would there be any way to apply this resize there?


 

 

<template>
  <v-app>
    <v-main>
      <div class="editor-container">
        <v-navigation-drawer permanent width="150">
          <v-list>
            <v-list-subheader>Elementos</v-list-subheader>
            <v-list-item
              prepend-icon="mdi-text"
              title="Texto"
              @click="addElement('text')"
              density="compact"
            />
            <v-list-item
              prepend-icon="mdi-image"
              title="Imagem"
              @click="showImageDialog = true"
              density="compact"
            />
            <v-divider class="my-2"></v-divider>
            <v-list-subheader>Layouts</v-list-subheader>
            <v-list-item
              prepend-icon="mdi-plus"
              title="Novo Layout"
              @click="newLayout"
              density="compact"
            />
            <v-list-item
              v-for="layout in savedLayouts"
              :key="layout.id"
              :title="layout.name"
              prepend-icon="mdi-file-outline"
              @click="loadLayout(layout)"
              density="compact"
            >
              <template v-slot:append>
                <v-icon size="small" @click.stop="deleteLayout(layout.id)">
                  mdi-delete
                </v-icon>
              </template>
            </v-list-item>
          </v-list>
        </v-navigation-drawer>
 
        <div class="canvas-area">
          <div class="canvas" ref="canvas">
            <div
              v-for="element in elements"
              :key="element.id"
              :ref="
                (el) => {
                  if (el) elementRefs[element.id] = el;
                }
              "
              class="canvas-element"
              :class="{ 'is-selected': selectedElement?.id === element.id }"
              :style="{
                left: `${element.x}px`,
                top: `${element.y}px`,
                width: `${element.width}px`,
                height: `${element.height}px`,
                position: 'absolute',
                zIndex: element.zIndex,
              }"
              @click="selectElement(element)"
            >
              <template v-if="element.type === 'text'">
                <div
                  contenteditable="true"
                  class="text-element"
                  :style="{
                    fontSize: `${element.fontSize}px`,
                  }"
                  @input="(e) => updateTextContent(element, e)"
                  @mousedown.stop
                  @focus="selectElement(element)"
                >
                  {{ element.content }}
                </div>
              </template>
              <template v-else-if="element.type === 'image'">
                <img
                  :src="element.src"
                  :style="{
                    width: '100%',
                    height: '100%',
                    objectFit: 'cover',
                    pointerEvents: 'none',
                  }"
                />
              </template>
              <template v-if="selectedElement?.id === element.id">
                <div
                  class="resize-handle tl"
                  data-direction="tl"
                  @mousedown="startResize($event, element, 'tl')"
                ></div>
                <div
                  class="resize-handle tr"
                  data-direction="tr"
                  @mousedown="startResize($event, element, 'tr')"
                ></div>
                <div
                  class="resize-handle bl"
                  data-direction="bl"
                  @mousedown="startResize($event, element, 'bl')"
                ></div>
                <div
                  class="resize-handle br"
                  data-direction="br"
                  @mousedown="startResize($event, element, 'br')"
                ></div>
              </template>
            </div>
          </div>
        </div>
 
        <v-navigation-drawer permanent location="right" width="150">
          <v-list v-if="selectedElement">
            <v-list-subheader>Propriedades</v-list-subheader>
            <v-list-item>
              <v-slider
                v-if="selectedElement.type === 'text'"
                v-model="selectedElement.fontSize"
                label="Tamanho da fonte"
                min="8"
                max="72"
                @update:modelValue="updateFontSize"
              />
            </v-list-item>
            <v-list-item>
              <v-text-field
                v-model="layoutName"
                label="Nome do Layout"
                placeholder="Meu Layout"
                class="mb-2"
                density="compact"
              />
              <v-btn color="primary" block @click="saveLayout" class="mt-2">
                Salvar Layout
              </v-btn>
            </v-list-item>
          </v-list>
        </v-navigation-drawer>
      </div>
    </v-main>
 
    <v-dialog v-model="showImageDialog" max-width="500">
      <v-card>
        <v-card-title>Adicionar Imagem</v-card-title>
        <v-card-text>
          <v-file-input
            v-model="imageFile"
            label="Selecione uma imagem"
            accept="image/*"
            prepend-icon="mdi-camera"
            @change="handleImageUpload"
          />
          <div class="text-center mt-4">
            <v-divider class="mb-4"></v-divider>
            <p class="text-body-2 mb-4">Ou insira uma URL de imagem:</p>
            <v-text-field
              v-model="imageUrl"
              label="URL da Imagem"
              placeholder="https://exemplo.com/imagem.jpg"
            />
          </div>
          <div class="text-center mt-4">
            <v-btn
              color="primary"
              @click="addImageElement"
              :disabled="!imageUrl && !imagePreview"
            >
              Adicionar Imagem
            </v-btn>
          </div>
        </v-card-text>
      </v-card>
    </v-dialog>
  </v-app>
</template>
 
<script setup>
import { refonMountednextTick } from 'vue';
import { useGsap } from '@/composables/useGsap';
import { Draggable } from 'gsap/Draggable';
 
const elements = ref([]);
const selectedElement = ref(null);
const canvas = ref(null);
const showImageDialog = ref(false);
const imageUrl = ref('');
const imageFile = ref(null);
const imagePreview = ref(null);
const layoutName = ref('Novo Layout');
const savedLayouts = ref([]);
const currentLayoutId = ref(null);
const elementRefs = ref({});
const draggableInstances = ref({});
 
const { gsap } = useGsap();
gsap.registerPlugin(Draggable);
 
const initDraggable = (element=> {
  const el = elementRefs.value[element.id];
  if (!elreturn;
 
  if (draggableInstances.value[element.id]) {
    draggableInstances.value[element.id].kill();
  }
 
  const draggable = Draggable.create(el, {
    type: 'x,y',
    bounds: canvas.value,
    allowNativeTouchScrolling: true,
    liveSnap: false,
    onPress(event) {
      if (event.target.classList.contains('resize-handle')) {
        event.stopPropagation();
        this.disable(); // ⚠️ Temporariamente desativa drag ao iniciar resize
      } else {
        selectElement(element); //  Agora garante que o elemento é selecionado
      }
    },
    onRelease() {
      this.enable(); //  Reativa Draggable após soltar
    },
    onDrag() {
      element.x = Math.round(this.x);
      element.y = Math.round(this.y);
    },
    onDragEnd() {
      saveLayout();
    },
  })[0];
 
  draggableInstances.value[element.id] = draggable;
};
 
const startResize = (eventelementdirection=> {
  alert('startResize');
  event.preventDefault();
  event.stopPropagation();
 
  const el = elementRefs.value[element.id];
  if (!elreturn;
 
  // 🔹 Garante que o elemento seja selecionado antes do resize
  selectedElement.value = element;
 
  // 🔹 Desativa temporariamente o Draggable para evitar conflitos
  if (draggableInstances.value[element.id]) {
    draggableInstances.value[element.id].disable();
  }
 
  const startX = event.clientX;
  const startY = event.clientY;
  const startWidth = element.width;
  const startHeight = element.height;
  const startLeft = element.x;
  const startTop = element.y;
 
  const onMouseMove = (e=> {
    const deltaX = e.clientX - startX;
    const deltaY = e.clientY - startY;
 
    if (direction.includes('r')) {
      element.width = Math.max(50startWidth + deltaX);
    }
    if (direction.includes('b')) {
      element.height = Math.max(50startHeight + deltaY);
    }
    if (direction.includes('l')) {
      element.width = Math.max(50startWidth - deltaX);
      element.x = startLeft + deltaX;
    }
    if (direction.includes('t')) {
      element.height = Math.max(50startHeight - deltaY);
      element.y = startTop + deltaY;
    }
 
    gsap.set(el, {
      width: element.width,
      height: element.height,
      x: element.x,
      y: element.y,
    });
  };
 
  const onMouseUp = () => {
    document.removeEventListener('mousemove'onMouseMove);
    document.removeEventListener('mouseup'onMouseUp);
 
    //  Reativa Draggable após soltar o mouse
    nextTick(() => {
      if (draggableInstances.value[element.id]) {
        draggableInstances.value[element.id].enable();
      }
    });
 
    saveLayout();
  };
 
  document.addEventListener('mousemove'onMouseMove);
  document.addEventListener('mouseup'onMouseUp);
};
 
const initResize = (element=> {
  const el = elementRefs.value[element.id];
  if (!elreturn;
 
  // Remove resizes antigos para evitar duplicação
  if (draggableInstances.value[element.id + '-resize']) {
    draggableInstances.value[element.id + '-resize'].forEach((d=> d.kill());
  }
 
  const resizeHandles = el.querySelectorAll('.resize-handle');
  const resizers = [];
 
  resizeHandles.forEach((handle=> {
    const direction = handle.dataset.direction;
    const resizer = Draggable.create(handle, {
      type: 'x,y',
      bounds: canvas.value,
      onPress() {
        this.startX = this.x;
        this.startY = this.y;
        element.startWidth = element.width;
        element.startHeight = element.height;
        element.startX = element.x;
        element.startY = element.y;
 
        // Desativa Draggable do elemento enquanto redimensiona
        if (draggableInstances.value[element.id]) {
          draggableInstances.value[element.id].disable();
        }
      },
      onDrag() {
        const deltaX = this.x - this.startX;
        const deltaY = this.y - this.startY;
 
        if (direction.includes('r')) {
          element.width = Math.max(50element.startWidth + deltaX);
        }
        if (direction.includes('b')) {
          element.height = Math.max(50element.startHeight + deltaY);
        }
        if (direction.includes('l')) {
          element.width = Math.max(50element.startWidth - deltaX);
          element.x = element.startX + deltaX;
        }
        if (direction.includes('t')) {
          element.height = Math.max(50element.startHeight - deltaY);
          element.y = element.startY + deltaY;
        }
 
        gsap.set(el, {
          width: element.width,
          height: element.height,
          x: element.x,
          y: element.y,
        });
      },
      onDragEnd() {
        // Reativa Draggable do elemento após redimensionar
        if (draggableInstances.value[element.id]) {
          draggableInstances.value[element.id].enable();
        }
        saveLayout();
      },
    })[0];
 
    resizers.push(resizer);
  });
 
  draggableInstances.value[element.id + '-resize'] = resizers;
};
 
const addElement = (type=> {
  const maxZ = Math.max(...elements.value.map((el=> el.zIndex || 0), 0);
  const newElement = {
    id: Date.now(),
    type,
    content: type === 'text' ? 'Novo texto' : '',
    src: '',
    width: 150,
    height: type === 'text' ? 100 : 150,
    fontSize: 16,
    x: 50,
    y: 50,
    zIndex: maxZ + 1,
  };
 
  elements.value.push(newElement);
  selectElement(newElement);
 
  nextTick(() => {
    initDraggable(newElement);
    initResize(newElement);
  });
};
 
const addImageElement = () => {
  if (imageUrl.value || imagePreview.value) {
    const maxZ = Math.max(...elements.value.map((el=> el.zIndex || 0), 0);
    const newElement = {
      id: Date.now(),
      type: 'image',
      content: '',
      src: imagePreview.value || imageUrl.value,
      width: 200,
      height: 200,
      x: 50,
      y: 50,
      zIndex: maxZ + 1,
    };
    elements.value.push(newElement);
    selectElement(newElement);
    showImageDialog.value = false;
    imageUrl.value = '';
    imageFile.value = null;
    imagePreview.value = null;
 
    nextTick(() => {
      initDraggable(newElement);
    });
  }
};
 
const handleImageUpload = (event=> {
  const file = event.target.files?.[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = (e=> {
      imagePreview.value = e.target.result;
      imageUrl.value = '';
    };
    reader.readAsDataURL(file);
  }
};
 
const selectElement = (element=> {
  if (element) {
    selectedElement.value = element;
    updateElementsOrder(element);
  }
};
 
const updateElementsOrder = (clickedElement=> {
  const maxZ = Math.max(...elements.value.map((el=> el.zIndex || 0));
  clickedElement.zIndex = maxZ + 1;
};
 
const updateTextContent = (elementevent=> {
  element.content = event.target.innerText;
  saveLayout();
};
 
const updateFontSize = (newSize=> {
  if (selectedElement.value && selectedElement.value.type === 'text') {
    selectedElement.value.fontSize = newSize;
    saveLayout();
  }
};
 
const saveLayout = () => {
  if (!layoutName.value.trim()) {
    layoutName.value = `Layout ${savedLayouts.value.length + 1}`;
  }
 
  const layoutData = {
    id: currentLayoutId.value || Date.now(),
    name: layoutName.value,
    elements: elements.value.map(cleanElementForStorage),
  };
 
  if (currentLayoutId.value) {
    const index = savedLayouts.value.findIndex(
      (l=> l.id === currentLayoutId.value
    );
    if (index !== -1) {
      savedLayouts.value[index] = layoutData;
    }
  } else {
    savedLayouts.value.push(layoutData);
    currentLayoutId.value = layoutData.id;
  }
 
  localStorage.setItem('savedLayouts'JSON.stringify(savedLayouts.value));
};
 
const loadLayout = (layout=> {
  elements.value = layout.elements.map((element=> ({ ...element }));
  selectedElement.value = null;
  currentLayoutId.value = layout.id;
  layoutName.value = layout.name;
 
  nextTick(() => {
    elements.value.forEach((element=> {
      initDraggable(element);
      initResize(element);
    });
  });
};
 
const deleteLayout = (layoutId=> {
  savedLayouts.value = savedLayouts.value.filter(
    (layout=> layout.id !== layoutId
  );
  localStorage.setItem('savedLayouts'JSON.stringify(savedLayouts.value));
  if (currentLayoutId.value === layoutId) {
    newLayout();
  }
};
 
const newLayout = () => {
  elements.value = [];
  selectedElement.value = null;
  layoutName.value = 'Novo Layout';
  currentLayoutId.value = null;
};
 
const cleanElementForStorage = (element=> {
  return {
    id: element.id,
    type: element.type,
    content: element.content,
    src: element.src,
    width: element.width,
    height: element.height,
    fontSize: element.fontSize,
    x: element.x,
    y: element.y,
    zIndex: element.zIndex,
  };
};
 
onMounted(() => {
  const layouts = localStorage.getItem('savedLayouts');
  if (layouts) {
    savedLayouts.value = JSON.parse(layouts);
  }
 
  nextTick(() => {
    elements.value.forEach((element=> {
      initDraggable(element);
      initResize(element);
    });
  });
});
</script>
 
<style lang="scss">
.editor-container {
  displayflex;
  height100%;
}
 
.canvas-area {
  flex1;
  padding20px;
  background-color#f5f5f5;
  overflowauto;
  displayflex;
  justify-contentcenter;
  align-itemsflex-start;
}
 
.canvas {
  width800px;
  height600px;
  backgroundwhite;
  box-shadow0 0 10px rgba(0000.1);
  positionrelative;
  overflowhidden;
}
 
.canvas-element {
  positionabsolute;
  user-selectnone;
 
  &.is-selected {
    outline2px solid #1976d2;
  }
 
  .text-element {
    padding8px;
    margin0;
    cursortext;
    overflowhidden;
    white-spacepre-wrap;
    word-wrapbreak-word;
    outlinenone;
    backgroundtransparent;
    width100%;
    height100%;
    box-sizingborder-box;
  }
}
 
.resize-handle {
  positionabsolute;
  width10px;
  height10px;
  background#1976d2;
  border-radius50%;
  z-index100;
 
  &.tl {
    top-5px;
    left-5px;
    cursornw-resize;
  }
 
  &.tr {
    top-5px;
    right-5px;
    cursorne-resize;
  }
 
  &.bl {
    bottom-5px;
    left-5px;
    cursorsw-resize;
  }
 
  &.br {
    bottom-5px;
    right-5px;
    cursorse-resize;
  }
}
</style>
 
Posted

Yeah, that indeed is the only file there, but as I mentioned there is far too much code there to go through and again this is not the simplest thing to achieve. That is far from being a minimal demo we need something small that we can tinker with but is also easy to follow and understand.

 

Maybe you can check this package in order to see if this helps and removes all the extra work:

https://www.npmjs.com/package/vue-draggable-resizable

 

Happy Tweening!

Posted
On 2/17/2025 at 2:47 PM, Rodrigo said:

Yeah, that indeed is the only file there, but as I mentioned there is far too much code there to go through and again this is not the simplest thing to achieve. That is far from being a minimal demo we need something small that we can tinker with but is also easy to follow and understand.

 

Maybe you can check this package in order to see if this helps and removes all the extra work:

https://www.npmjs.com/package/vue-draggable-resizable

 

Happy Tweening!

Is gsap really not supported?

Posted

Hi,

 

GSAP is supported, but due to time issues we can't really offer support for a demo that has over 250 lines of JS alone, that is why we ask for a minimal demo, we just don't have time to go through all that code trying to figure how it works and then comb through it and find out what could be the issue and solve it. That falls into the category of paid consulting not support. What we do in these free forums is answer questions and doubts regarding how GSAP and it's tools work, not solve implementation problems in large code bases or full apps. We don't need to see your entire code base, just a few colored divs that clearly illustrate the problem you're having.

 

As mentioned before I urge you to create a small demo that clearly illustrates the problem you're having in the simplest possible way with as little code as possible.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...