explicitClick to confirm you are 18+

Maximizing modal dimensions on the fly

omadridAug 20, 2019, 10:22:35 PM
thumb_up53thumb_downmore_vert

(Disclaimer: as is the nature of code, this blog post and the screenshots it contains will likely be out of date soon.  But the concepts and logic are timeless :) )

THE GOAL

Create a modal so users can stay within the context of the newsfeed when they click on media posts (i.e. images and videos). Thinking ahead, we also want to add a paging functionality in the near future so users can click through other media posts from the same channel without leaving the modal, so the modal should be able to seamlessly handle all sorts of media shapes and sizes.

A finished modal in the wild (photo credit: @elizabeth)

HANSEL & GRETEL DEVELOPMENT

Artist Paul Gauguin once said: "Art is either plagiarism or revolution." I agree. And since I prefer to save my revolutionary energies for when they'll really matter, I unapologetically modelled the modal off of an existing design from another social media site that Must Not Be Named (since that particular wheel is already working fine, why reinvent it?). Since their code isn't open source, I had to work backwards using the scant breadcrumbs they left in the developer tools.

I could see that they were dynamically setting the width/height of certain elements inline, and that the dimensions reacted in certain predictable ways when I resized my browser window.

To figure out exactly how their design worked, I created a private test album with a handful of images of various shapes and sizes (e.g. a very small square, a really wide/short image, a really tall/narrow image, etc.). 

A very small 200 x 200px square image, for testing purposes

I then went into my browser-of-choice's dev tools and observed the dimensions of the elements with dynamic inline styles (let's call those elements (A) media, (B) stage, and (C) modal) as I slooooooowly resized my browser window, observing when certain milestones occurred - e.g. when the modal starts shrinking, when the modal stage reaches minimum size, when stage padding appears, etc. Each time I reached a milestone, I took note of the dimensions of A, B and C in a spreadsheet.

The widths and heights of the (A) media, (B) stage, and (C) modal elements are calculated dynamically

Once I had a few images worth of measurements in a spreadsheet, I more or less stared at the numbers and ran them through various formulas to decipher the relationship between them until a predictable pattern emerged and I was ready to write the code.

CALCULATING DIMENSIONS

We want the media to fill as much of the modal's available space as possible in any tablet/desktop screen, regardless of screen orientation (by available space, I mean whatever isn't occupied by the content sidebar - which has a fixed width of 360px - or the minimum 20px margin that surrounds the modal). We also don't want the stage to get too small (and by too small, I mean the black background 'stage' that surrounds the media shouldn't go below 660px width or 520px height.)

The first step was to create a calculateDimensions() function that is triggered when the modal is created and on also every window resize event thereafter. The calculateDimensions() function works as a sort of dispatcher that processes the pixel values of the height/width of the (A) media/(B) stage and pushes them through various smaller functions as needed in order to reach the end result. (We can put the (C) modal dimensions aside until the end, as the modal height always equals the stage height, and the modal width always equals the stage width plus the 360px of the content sidebar).

There are so many moving parts in this component that I included an above-average quantity of inline comments to help out whoever may need to adjust the code in the future (and that will probably be future-me, so thanks, past-me!)

There are a number of ways to do go about this, and I ultimately decided to just pick an axis to start with and go from there (I opted for height). The resulting process is as follows:

1. Set fixed minimums for stage height (520px) and stage width (660px), and set maximum stage width (calculated, based on window width)

2. Set the stage/media heights as tall as possible within the constraints of the window (if the window is so small that the heights end up being smaller than the 520px minimum, set the heights to minimum instead). Then use the media's intrinsic aspect ratio to calculate the widths.

Start by maximizing heights of the stage and media

3. Now we check the widths to see if they are wider than the maximum width or narrower than the minimum width.

3a. If the scaled widths are wider than the maximum, set the widths to the maximum and rescale the heights to preserve the aspect ratio.

Handle media that's too wide

3b. If the scaled widths are narrower than the minimum, set them to the minimum and rescale the heights to preserve the aspect ratio (and potentially continue to shrink the height until it hits minimum).

Handle narrow widths

Throughout the process, rescaling is done with these simple helper functions:

Scale helpers

In the end, the calculateDimensions() flow does involve a small amount of "guess and check" grunt work (what with the potential for rescaling the dimensions more than once), but this way we are able to process all media items in a single workflow, regardless whether they are in portrait or landscape orientation, which will hopefully help make it easier to maintain and build upon in the future.

LOADING UX

Now that we have the dimensions calculations working, we can think about how to present them to the user. I didn't want the modal to abruptly resize itself to fit its content after it's loaded, so I accessed the intrinsic dimensions of the media from the activity source before the modal opens. This way, when it does open, the modal already knows how big it wants to be.

For some images, the dimensions are already stored in the Minds activity entity's custom_data object. For images without custom_data dimensions, I grab the dimensions from the activity's <img> HTML element (myImgElement.naturalHeight and myImgElement.naturalWidth) after angular's loaded event has fired.

Similarly, for videos, after the <video> element's metadataLoaded event has fired, I get the intrinsic dimensions from myVideoElement.videoHeight and myVideoElement.videoWidth.

I also used these same events (loaded & metadataLoaded) in the modal component itself, to trigger a fade-in animation so the experience is as smooth as possible. 

FAUX URL

Another 'featurette' of the modal is that when it's open, the browser url changes to point to the relevant media page instead of the newsfeed (or wherever you were when you opened the modal) - this way, users can easily copy the url and share a link to a specific post, as opposed to accidentally sharing a link to the newsfeed. When they close the modal, or click a link from inside of it, the 'faux url' is cleared from the browser history, as though it never happened (so no one is confused if they hit the back button and end up at a page they've never been to).

To achieve this, I used angular's Location service's replaceState() method, which allows me to change the url to point to the media page without actually redirecting the page. Then I use an rxjs Subscription to subscribe() to all router events, which lets me know when a NavigationStart event is fired (i.e. the user has clicked a link from within the modal). When that happens, I use replaceState() again to 'fix' the browser history by replacing the faux url with the actual url of the page they're currently on (e.g. the newsfeed), and then use angular's Router service's navigate() method to go to the intended destination. 

Handling the faux url

Note: the navigatedAway boolean acts as a gate that allows only one NavigationStart event to trigger this process, thereby preventing the browser from getting caught in an endless redirection loop.

REFERENCE

All of the changes made to incorporate the modal can be seen in front merge request 467.  The modal component itself lives inside the media module, and its specific files are here: modal.component.ts, modal.component.html, modal.component.scss.  For even further reference, check out the code in the Minds front repository on gitlab.

Thanks for reading :)