Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split layout get splitter-position after moving the splitter manually #1517

Closed
EvaGillen opened this issue Feb 12, 2019 · 24 comments · Fixed by #5158
Closed

Split layout get splitter-position after moving the splitter manually #1517

EvaGillen opened this issue Feb 12, 2019 · 24 comments · Fixed by #5158
Labels
DX Developer experience issue enhancement New feature or request Impact: High vaadin-split-layout

Comments

@EvaGillen
Copy link

I am trying to get the position of the splitter after moving the splitter manually.
I know how to set the position. And I know that I can get the position with e.g. primaryComponent.getElement.getStyle.get(ElementConstants.STYLE_HEIGHT). But that's just the position I set on server-side.

I need the new position after moving the splitter.

@EvaGillen EvaGillen changed the title Split layout get position after moving the splitter manually Split layout get splitter-position after moving the splitter manually Feb 12, 2019
@mehdi-vaadin
Copy link
Contributor

Currently, SplitLayout doesn't have such a feature.

@samulivaadin
Copy link
Contributor

Would suggest that the SplitterDragendEvent for SplitLayout.addSplitterDragendListener would be provided with changed split position. Otherwise this is not possible to get split position at server when it has been changed by the client as accessing the style wont work if that has not been changed from the server.

@pleku
Copy link
Contributor

pleku commented Mar 7, 2019

Now that DomEventRegistration allows you to add eventData dynamically, it could be relatively easy to fix this without any changes to the web component. To but to get the height and width of the primary and secondary component to be available in the event object that is fired, there would have to be some new API or at least the existing addSplitterDragendListener(ComponentEventListener<SplitterDragendEvent<SplitLayout>> listener) API will be difficult to change because the SplitterDragendEvent is generated. It would get too nasty to try to implement this to the component API generator, so probably it would make sense to deprecate the old event listener API and instead introduce some new API that provides an event that will have the API for the new sizes.

@Duncol
Copy link

Duncol commented Mar 14, 2019

Now that DomEventRegistration allows you to add eventData dynamically, it could be relatively easy to fix this without any changes to the web component.

Correct me if I am wrong, but this seems not possible as SplitterDragendEvent is fired as a CustomEvent with no extra details.

@pleku
Copy link
Contributor

pleku commented Mar 14, 2019

Correct me if I am wrong, but this seems not possible as SplitterDragendEvent is fired as a CustomEvent with no extra details.

Yes, but you can have the event object and it will give you access all the details you need. For instance, you can add use event.currentTarget._primaryChild.offsetWidth and event.currentTarget._secondaryChild.offsetWidth or same for height. It might be better to not use the private API to access the elements, but I think those are always around (TM) when there is a splitter to drag.

PS. I'm not sure at this time of the evening, whether it should be offset, client or scroll size that one will be interested in.

@pleku
Copy link
Contributor

pleku commented Mar 14, 2019

Actually, you can even replace event.currentTarget with element https://vaadin.com/api/platform/13.0.1/com/vaadin/flow/component/EventData.html

@samulivaadin
Copy link
Contributor

Since there is the setSplitterPosition(double) method with percentage, one would think that the event would give also changed value in same unit (double %)

@pleku
Copy link
Contributor

pleku commented Mar 15, 2019

Very good point Samuli! That will be easy enough to calculate from the widths/heights of the two parts. And I think it should be offsetWidth/offsetHeight that is used. That should be accurate enough even though it doesn't take animations into account.

@Duncol
Copy link

Duncol commented Mar 15, 2019

@pleku Thank's I'll definitely check this out, as now I'm using workaround that establishes MutationObserver on client-side, that launches custom event when style changes (the flex attr), but only if there is no pointerEvents attr. This gives me one event, exactly on dragend of the splitter :). The one benefit of this is that I calculate the percentage also on client side, so it is a little bit cleaner in event handling section in Java code :).

@Duncol
Copy link

Duncol commented Mar 18, 2019

@pleku I'm not sure what you meant with "accessing the event". The addSplitterDragendListener does not return com.vaadin.flow.dom.DomListenerRegistration, but com.vaadin.flow.shared.Registration. Also the event object passed to the registered listener in addSplitterDragendListener is not DomEvent. The only way I see is to (or equivalent using @eventdata): getElemenet().addEventListener("splitter-dragend").addEventData("element"). Is that what you've had in mind?

EDIT: Both options returned a Vaadin error:

client-28831013082792340517B7074033EF04.cache.js:198 (String) : Message JsonObject contained a dom node reference which should not be sent to the server and can cause a cyclic dependecy.

EDIT2: Ok, so there is a bug for this?
vaadin/flow#4214

@pleku
Copy link
Contributor

pleku commented Mar 19, 2019

Actually @Duncol I did have that in mind, but I meant that the implementation of SplitLayout and SplitterDragEndEvent would be changed based on that, and then released as a new version of the Java integration.

getElement().addEventListener("splitter-dragend").addEventData("element")
won't work, because as the error message states, the client cannot serialize a DOM element which has cyclic references to different things you don't even need. You should use something more specific like

getElement().addEventListener("splitter-dragend").addEventData("element._primaryChild.offsetWidth")

@voodoohoo
Copy link

@Duncol Would you share your workaround? We're also in need to get the actual SplitterPosition, but we're no JavaScript experts to write this ourselves.

@Duncol
Copy link

Duncol commented Jun 19, 2019

@voodoohoo Well, I am not an JS expert either, but sure. I cannot share with you the code, but I can point out what I've done to achieve this.

When you drag the the splitter, the the 'flex' style property of both primary and secondary component change.
The trick is to listen on this style change for primary OR secondary component with mutation observer and dispatch you own event with the proportion value. I prefer primary component, as Java API utilizes it as well in setSplitterPosition method so I get the desired ratio almost straight ahead.

When you receive the style change, you refer to the 'flex-basis' which is in pixels. In order to align with the Java-side API you need to calculate the proportion (percentage) according to the primaryComponent.parent.offsetWidth/offsetHeight. Crucial thing here is to dispatch such event only if the style has no 'pointer-events' attribute, as this is when the drag ends (i.e. it will fire only once, when user drops the splitter).

Next, you launch a custom event with this value, for which you listen using getElement().addEventListener() on the Java side.
Key note here is to use some unique custom event (e.g. with SplitLayout id as suffix) for each split layout OR provide some additional value in the event to know for which one the splitter position changed. Personally, I prefer option one, as I do not need some if/else structure in Java to determine the source of this custom event.

EDIT: Actually given the fact that you can launch this event from the e.g. primary component context, and then you listen for it in the same component (just on Java side this time) I think you do not need more than one custom event.

TL;DR

  • Dispatch custom event on 'flex-basis' change (MutationObserver; style attr) with calculated proportion
  • Listen to this event (getElement().addEventListener()),
  • On the event, setSplitterPosition() using the received value.

Hope that helps. If you'll have some sample code yourself, but still having trouble at some point I can help.

@voodoohoo
Copy link

@Duncol Thanks for your in detail description. We're going to check if this will working for us.

@stefanuebe
Copy link
Contributor

stefanuebe commented Jul 31, 2019

Here is some example workaround code (pure Java based with server side set JS) - without any warranties of working everywhere and under each circumstance ;)

The example uses custom split class (tbh I did not get it work with element dom event listeners), some custom dom event and a listener.

Init code, e.g. in constructor, post construct or on attach. It currently sends only the primary component's width, but is easily extendible to have also the secondary component's one:

 CustomSplitLayout mainLayout = new CustomSplitLayout(primaryComponent, secondaryComponent);
        mainLayout.setOrientation(Orientation.HORIZONTAL);
        mainLayout.getElement().executeJs("this.addEventListener('iron-resize', e => {" +
                /* currently only primary components width. Extend by [1] if you need also second components width */
                "   this.leftWidth = e.target.childNodes[0].style['flex-basis'];" +
               /* uncomment this, if you want to be informed on splitter drag end only instead all the time*/
               //                "});" +
               //                "" +
               //                "this.addEventListener('splitter-dragend', e => {" +
                "   this.dispatchEvent(new CustomEvent('splitter-resized', {" +
                "       detail: {" +
                "           leftWidth: this.leftWidth" +
                "       }" +
                "   }))" +
                "})");

        mainLayout.addSplitterResizedListener(event -> {
            double leftWidth = event.getLeftWidth();
            // do something with the left width
        });

Custom class and custom dom event. Please be aware that the dom listener currently only handles "px" endings. Extend, if you need also initially other values or init your components with px based flex-basis values..

public static class CustomSplitLayout extends SplitLayout {
        public CustomSplitLayout() {
        }

        public CustomSplitLayout(Component primaryComponent, Component secondaryComponent) {
            super(primaryComponent, secondaryComponent);
        }

        public Registration addSplitterResizedListener(ComponentEventListener<SplitterResizedEvent> listener) {
            return addListener(SplitterResizedEvent.class, listener);
        }
    }

    @DomEvent("splitter-resized")
    @Getter
    public static class SplitterResizedEvent extends ComponentEvent<SplitLayout> {
        private final double leftWidth;

        /**
         * Creates a new event using the given source and indicator whether the
         * event originated from the client side or the server side.
         *
         * @param source     the source component
         * @param fromClient <code>true</code> if the event originated from the client
         */
        public SplitterResizedEvent(SplitLayout source, boolean fromClient, @EventData("event.detail.leftWidth") String leftWidth) {
            super(source, fromClient);

            this.leftWidth = Double.parseDouble(StringUtils.remove(leftWidth, "px"));
        }
    }

@Enerccio
Copy link

Enerccio commented Mar 23, 2020

this still hasn't been resolved? What is the point of addSplitterDragendListener when you get nothing?

The more I try to work with vaadin flow, the more I find that half of stuff is half baked and missing... why?

@simasch
Copy link

simasch commented Mar 26, 2020

I'm support the comment of @Enerccio
The SplitterDraggendListener is useless if we don't get the position.

@voodoohoo
Copy link

Hi,

we've subclassed SplitterLayout to add the needed functionality.
After initializing, we call an addDragEventListener

  private void addDragEventListener() {
    getElement()
        .addEventListener(
            "splitter-dragend",
            e -> {
              double pSize = e.getEventData().getNumber(PRIMARY_SIZE);
              double sSize = e.getEventData().getNumber(SECONDARY_SIZE);
              double totalSize = pSize + sSize;
              double primarySize = Math.round((pSize / totalSize) * 100.0);
              setPosition(primarySize);
            })
        .addEventData(PRIMARY_SIZE)
        .addEventData(SECONDARY_SIZE);
  }

this calls the setPosition(double) method when dragged

  public void setPosition(double primarySize) {
    setFlex(getPrimaryComponent(), primarySize);
    setFlex(getSecondaryComponent(), 100.0 - primarySize);
    setMiscValues(primarySize);
    savePosition(primarySize);
    eventBus.post(new EventSizeChanged(id, primarySize));
    splitterPositionChanged(primarySize);
  }

Some constants:

private static final String PRIMARY_OFFSET_HEIGHT = "element._primaryChild.offsetHeight";
  private static final String SECONDARY_OFFSET_HEIGHT = "element._secondaryChild.offsetHeight";
  private static final String PRIMARY_OFFSET_WIDTH = "element._primaryChild.offsetWidth";
  private static final String SECONDARY_OFFSET_WIDTH = "element._secondaryChild.offsetWidth";

  private String PRIMARY_SIZE = PRIMARY_OFFSET_HEIGHT;
  private String SECONDARY_SIZE = SECONDARY_OFFSET_HEIGHT;

There is some specific code around like savePosition (position is saved per user) and the publishing of an EventSizeChanged via Guava EventBus.

This works well for us and the subclass can easily be used around the projects.
Hope this helps.

I'd also really appreciated that this essential things get done before adding lots of other functionalities. The API quality and completeness of Vaadin 6-8 has never been reached with Vaadin 10-14.
We created lots of custom elements while migrating from Vaadin7/8. The thought of an easy to use server side java framework lacks of completeness and the Dev now is forced to use JavaScript to add basis Api functionality.

It is well over a year since we opened this issue :-/

Erik

@simasch
Copy link

simasch commented Mar 26, 2020

@voodoohoo Thanks a lot for the code. This really helps.

We are also facing a lot of issues like Spring integration that are very long open.

@cjpelaezrivas
Copy link

@voodoohoo Thank you for your solution. We have slightly modified it to fit our needs. Hope it helps someone.

public class SplitLayoutP extends SplitLayout {

  private static final String PRIMARY_OFFSET_HEIGHT = "element._primaryChild.offsetHeight";
  private static final String SECONDARY_OFFSET_HEIGHT = "element._secondaryChild.offsetHeight";
  private static final String PRIMARY_OFFSET_WIDTH = "element._primaryChild.offsetWidth";
  private static final String SECONDARY_OFFSET_WIDTH = "element._secondaryChild.offsetWidth";

  private String PRIMARY_SIZE = PRIMARY_OFFSET_WIDTH;
  private String SECONDARY_SIZE = SECONDARY_OFFSET_WIDTH;

  private double splitterPosition;
  private double primarySizePixel;
  private double secondarySizePixel;

  public SplitLayoutP() {
    super();
    init();
  }

  private void init() {
    getElement().addEventListener("splitter-dragend", e -> {
      this.primarySizePixel = e.getEventData().getNumber(PRIMARY_SIZE);
      this.secondarySizePixel = e.getEventData().getNumber(SECONDARY_SIZE);

      double totalSize = this.primarySizePixel + this.secondarySizePixel;
      this.splitterPosition = Math.round((this.primarySizePixel / totalSize) * 100.0);
    })
        .addEventData(PRIMARY_SIZE)
        .addEventData(SECONDARY_SIZE);
  }

  @Override
  public void setOrientation(Orientation orientation) {
    super.setOrientation(orientation);

    if (orientation == Orientation.HORIZONTAL) {
      PRIMARY_SIZE = PRIMARY_OFFSET_WIDTH;
      SECONDARY_SIZE = SECONDARY_OFFSET_WIDTH;
    } else {
      PRIMARY_SIZE = PRIMARY_OFFSET_HEIGHT;
      SECONDARY_SIZE = SECONDARY_OFFSET_HEIGHT;
    }
  }

  @Override
  public void setSplitterPosition(final double position) {
    super.setSplitterPosition(position);

    this.splitterPosition = position;
  }

  public double getSplitterPosition() {
    return splitterPosition;
  }

  public double getPrimarySizePixel() {
    return primarySizePixel;
  }

  public double getSecondarySizePixel() {
    return secondarySizePixel;
  }
}

@vaadin-bot vaadin-bot transferred this issue from vaadin/vaadin-split-layout-flow Oct 6, 2020
@vaadin-bot vaadin-bot transferred this issue from vaadin/vaadin-split-layout May 19, 2021
@vaadin-bot vaadin-bot transferred this issue from vaadin/web-components May 21, 2021
@enver-haase
Copy link
Contributor

Dear @vaadin-bot , please remove the 'enhancement' label as it is a severe usability bug, which I am encountering in a migration project from Vaadin 7 (!) to Vaadin 20 (!).
@pleku

@sdorner
Copy link

sdorner commented Oct 6, 2021

@voodoohoo and @cjpelaezrivas Thanks for your code, it helped a lot. A minor addition I had to make for Orientation.VERTICAL to work, was to add the height-offsets to the event data as well.

@Frettman
Copy link

Frettman commented May 4, 2022

For me this is also a missing feature during a Vaadin 7/8 migration (to 23). As others have pointed out, the SplitterDragendEvent is quite pointless without the splitter position. So I'd also consider this a flaw and not just an enhancement. Anyway, thank you for the provided workarounds. They seem simple enough which makes it even more strange that this hasn't been fixed yet.

@chkpnt
Copy link

chkpnt commented May 24, 2023

Thanks for the cookbook entry. Nevertheless, I think that the splitter position should be available in SplitterDragendEvent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DX Developer experience issue enhancement New feature or request Impact: High vaadin-split-layout
Projects
None yet
Development

Successfully merging a pull request may close this issue.