GrapesJS / grapesjs

Free and Open source Web Builder Framework. Next generation tool for building templates without coding
https://grapesjs.com
BSD 3-Clause "New" or "Revised" License
22.37k stars 4.05k forks source link

Copy Paste Components #1855

Closed ssabrewolf closed 5 years ago

ssabrewolf commented 5 years ago

Hello Grapesjs Team Ive seen the regular copy paste works as long as is made in the same page, Is it possible implement a a copy paste between 2 different pages with the editor opened

Thanks in advance

arthuralmeidap commented 5 years ago

You can implement this very easily.

There are 2 commands that do this job. You can override them to work as you expect.

Take a look at the CopyComponent command. It copies the selected block to a property called clipboard.

In your case instead of saving everything into a property you will need to save the components into the OS clipboard and them getting them back to paste properly

ssabrewolf commented 5 years ago

Hi thanks for the feedback, unfortunately that approach wont work, before posting i checked the core:copy implementation and its based on storing an object reference on the clipboard, of course this reference only exist on the current page, thats why trying to paste in other page doesnt work despite the clipboard having data, i posted this question in hope there is some kind of core method for serializing component into string along with its css attributes, i found this code to extract a component css but it return a lot extra css from body and others(like 7000 chars) that could create a css collision with target page editor.CodeManager.getCode(component, 'css', {cssc: editor.CssComposer});

Regards

arthuralmeidap commented 5 years ago

Well, I know, at least, one workaround for this problem.

You can use the component toHTML method to get the string representation that you need.

For the css, since I don't know a way to get only the css for that component, you could inline the css for your html and it will inline only for that element because you only have the html for that component.

I'm not sure if I was clear enough

ssabrewolf commented 5 years ago

Hi yes, actually i had implemented that way overriding the toHTML method and creating a targeted css directory applied on target page in element scope using the same recursive function with getClassRule method, but wanted to be sure there was not a better option provided by the core, cause depending in the original copied component nested level, the elements gets funky autosize behaviours when the wrapper on the target page varies, e.g when a cell is removed from a row

Regards

artf commented 5 years ago

Is it possible implement a a copy paste between 2 different pages with the editor opened

Well, if for 2 different pages you mean 2 different tabs, I don't think so. Obviously, you're dealing with different editor instances so you have to find your way to make them talk with each other (eg. localStorage if you're on the same domain)

japo32 commented 5 years ago

This is our implementation of using localStorage in case someone needs the solution.

      function newCopy(selected) {
        window.localStorage.setItem('grapesjs_clipboard', JSON.stringify(selected));
      }
      function newPaste(selected) {
        var components = JSON.parse(window.localStorage.getItem('grapesjs_clipboard'));

        if (components) {
          if (selected && selected.attributes.type !== 'wrapper') {
            var index = selected.index();
            // Invert the order so last item gets added first and gets pushed down as others get added.
            components.reverse();
            var currentSelection = selected.collection;
            components.forEach(comp => {
              // TODO: Add check for validity of paste.
              var added = currentSelection.add(comp, {at: index + 1});
              editor.trigger('component:paste', added);
            });
            selected.emitUpdate();
          } else {
            // No components are selected so just insert at the end.
            editor.addComponents(components);
          }
        }
      }

      const commands = editor.Commands;
      commands.add('core:copy', editor => {
        const selected = [...editor.getSelectedAll()];
        //Filter out components that are not copyable.
        var filteredSelected = selected.filter(item => item.attributes.copyable == true);
        if (filteredSelected.length) {
          newCopy(filteredSelected);
        }
      });
      commands.add('core:paste', editor => {
        const selected = editor.getSelected();
        newPaste(selected);
      });

Clipboard API would have been a great cross browser solution but it's not yet widely used.

MarksEliel commented 2 years ago

updates? no css paste

ssabrewolf commented 2 years ago

@MarksEliel if that works to you i can share but it has a jQuery dependency not too hard to skip thats why i didnt share it , the solution is not 100% perfect so if you could enhance it and reshare to the community i can share it

Dakri commented 2 years ago

@ssabrewolf @MarksEliel
I'm also very interested in a solution including the CSS. I am currently analyzing the example from @japo32 , trying to find my own solution for it.

ahmadlimbada commented 2 years ago

I was able to implement @japo32 solution however still css is not being pasted, had some trial & erros but it's going no where. it would be great if anyone can provide any idea. @artf @ssabrewolf @MarksEliel @arthuralmeidap

MarksEliel commented 2 years ago

Guys, I did it that way and it's working. https://prnt.sc/BATrbgkyj7aF

lexoyo commented 2 years ago

I didn't try it, I just read the code

You don't trigger the paste event when pasting in the body

Also the css is added in a style tag after each paste... There must be a better way to handle the styles, I'll take a look soon

skru commented 2 years ago

I came up with this, a bit heavy handed. This would be so easy if I could find a way of converting a Backbone model to JSON and back, any ideas??

Extended @japo32's idea, It just grabs style from components and adds them back after they've been rendered:

`// COPY PASTE COMPONENTS/STYLE BETWEEN PAGES 

const getStyles = (components) => {
  // recurse down through components and store styles in temp attribute
  components.forEach(component => {
    const recurse = (comp) => {
      // if component has any styling
      if (Object.keys(comp.getStyle()).length !== 0) comp.attributes.savedStyle = comp.getStyle()
      if (comp.get("components").length) {
        comp.get("components").forEach(child => {
          recurse(child)
        })
      }
    }
    recurse(component)
  })
  return components
}

const setStyles = (component) => {
  // recurse down and re-apply style back to components
  const recurse = (comp) => {
    if ('savedStyle' in comp.attributes) {
      comp.setStyle(comp.attributes.savedStyle)
      delete comp.attributes.savedStyle
    }
    if (comp.attributes.components.length) {
      comp.attributes.components.forEach(child => {
        recurse(child)
      })
    }
  }
  recurse(component)
}

const newCopy = (selected) => {
  window.localStorage.setItem('zcc-grapesjs-clipboard', JSON.stringify(selected));
}

const newPaste = (selected) => {
  let components = JSON.parse(window.localStorage.getItem('zcc-grapesjs-clipboard'));
  if (components) {

    if (selected && selected.attributes.type !== 'wrapper') {
      var index = selected.index();
      // Invert the order so last item gets added first and gets pushed down as others get added.
      components.reverse();
      var currentSelection = selected.collection;
      components.forEach(comp => {
        if (currentSelection) {
          var added = currentSelection.add(comp, { at: index + 1 });
          editor.trigger('component:paste', added);
          setStyles(added)
        }

      });
      selected.emitUpdate();
    } else {
      components = editor.addComponents(components);
      components.forEach(comp => {
        setStyles(comp)
      })
    }
  }
}

const commands = editor.Commands;

commands.add('core:copy', ed => {
  const selected = getStyles([...ed.getSelectedAll()])
  let filteredSelected = selected.filter(item => item.attributes.copyable == true);
  if (filteredSelected.length) {
    newCopy(filteredSelected);
  }
});

commands.add('core:paste', ed => {
  const selected = ed.getSelected();
  newPaste(selected);
});`