# apostrophe-modal (browser)

# Inherits from: apostrophe-context

apostrophe-modal is a base class for modal dialog boxes. Define a new type that extends apostrophe-modal, set the source option to the name of a server-side route that outputs suitable HTML, extend the beforeShow method to add custom event handlers and dynamic content, and extend saveContent to take action when the save button is clicked.

Where the markup comes from

The source option is combined with the action option to arrive at the URL for fetching the modal's markup:

/action/source

action is usually pushed from the server side via pushBrowserCall, so that the server-side code can just implement its route via self.route. The route is a POST route. Any data present in the body option is passed as POST parameters.

Examples

The apostrophe-tags, apostrophe-widgets and apostrophe-pieces modules provide excellent examples of how modals are defined, created and populated.

An alternative to "save and cancel"

An alternative approach: if a single save operation doesn't make sense, you can implement buttons that perform actions immediately and use a "cancel" button labeled Done to close the modal.

When a modal creates another modal, they "stack," unless the new modal has the transition: slide option and the top modal already on the stack has the apos-modal-slideable CSS class, in which case the new modal "slides in" creating a breadcrumb trail.

Subclasses of apostrophe-modal can also provide a $view jQuery reference, in which case the new "modal" actually populates that div directly and doesn't actually block the page or display in its own modal dialog box. This is convenient when you wish to build up modals by composition.

# Methods

# getSource(callback)

Fetch rendered HTML to populate self.$el with the actual content of the modal. The HTML is fetched from:

self.options.action + '/' + self.options.source

Via a POST request.

If a self.body object exists it is passed to the server side as POST parameters.

Invoked for you as the first step of afterConstruct.

# enableGlobalEventsOnce()

Enables support for the escape key and click-outside-to-cancel behaviors. The handlers are installed on first use and then reused by any nested modals. Invoked for you as part of afterConstruct.

# disableGlobalEvents()

Removes the global event handlers for ESC and click-outside- to-close. Invoked when the last open modal closes.

# enableButtonEvents()

Add event handlers for the cancel and save buttons, [data-apos-cancel] and [data-apos-save]. If the save also has the [data-next] attribute, the self.next() method is invoked with no arguments after the normal save-and-close operation. This is meant to allow "save and create another" behavior, which is popular with experienced users.

In this base class self.next() is not implemented.

# enableBreadcrumbEvents()

Enable clicks on the breadcrumb trail [data-modal-breadcrumb], which is present when "stacked" modals "slide in" instead by setting the transition option to slide when constructing the modal. The breadcrumb trail can be used to back up to any point in the series of slides. All modals after that point have their cancel operation invoked, starting with the last/newest modal.

# captureTitle()

Fetches the title of the modal from the element with [data-modal-title] and records it in self.title. Called as part of afterConstruct.

# captureControls()

Locates the div that contains the controls for saving, cancelling, and other top-level operations on this modal and stores a jQuery reference to it in self.$controls. Part of the implementation of the slide transition, which moves these controls to a shared div outside of the individual slide modals for layout reasons.

# captureFilters()

Locates the div that contains the filters for this modal and stores a jQuery reference to it in self.$modalFilters. Part of the implementation of the slide transition, which moves these filters to a shared div outside of the individual slide modals for layout reasons. The name modalFilters avoids a bc break with the pieces manager modal.

# captureInstructions()

Locates the div that contains the instructions (the explanatory caption) for the modal and stores a jQuery reference to it in self.$instructions. Part of the implementation of the slide transition, which moves these controls to a shared div outside of the individual slide modals for layout reasons.

# resetEl()

Part of the implementation of the slide transition. When sliding a new modal in, self.$el is reset to the [data-modal-content] element within the modal, so that event handlers relying on self.$el work reasonably after the original modal div is diced up to move the controls, filters and instructions to one area of the slide container and the content div to another.

If this modal does not have the { transition: 'slide' } option, or there is no modal already open with the apos-modal-slideable CSS class, this method does nothing.

The original div is captured in self.$shell, which is currently used only as a test for whether the modal is a slide.

# getSlideableAncestorEl()

Part of the implementation of the slide transition. Checks the most recent non-sliding modal in the stack to see whether it has the apos-modal-slideable CSS class and, if so, returns a jQuery reference to that modal.

# setSelfReference()

Records a reference to the modal in the aposModal jQuery data attribute of self.$el, the div corresponding to the modal. Invoked by afterConstruct.

# enableLifecycleEvents()

Adds jQuery event handlers to self.$el, the div corresponding to the modal, for the aposModalCancel and aposModalHide events.

# beforeunload()

# getLastSlide()

Return the last slide, or the modal itself if it has no nested slides. Returns the apostrophe-modal object, not a jQuery element. Use findSafe so we are not faked out by nested views.

# getSlides()

Returns the apostrophe-modal objects corresponding to each slide nested in this modal, which presumably is a slide parent modal (one with the apos-modal-slideable CSS class). The slides are returned in the order they slid in, so the deepest (currently visible) slide is the last in the array. Used by the breadcrumb trail mechanism that displays the titles of all of the slides and allows clicking to jump backwards, closing intervening slides.

# show()

Displays the modal. The enhance Apostrophe event is triggered, with self.$el as the argument, allowing progressive enhancement to take place. If the modal has a $view option, it is appended to that div rather than displaying as a modal normally would.

Otherwise, if the transition option is set to slide and the top modal already on the stack has the apos-modal-slideable CSS class, the new modal "slides in," adding its title to the breadcrumb trail.

Otherwise, the modal is pushed onto the stack, appearing on top of the previous modal if any.

Note that self.beforeShow(callback) and self.afterShow() are provided for your overriding convenience. Usually it is better to override these rather than changing the implementation of self.show() to do extra work.

# slideIn()

Invoked for you by self.show(), this method causes the modal to "slide in" and add itself to the breadcrumb trail if the top modal on the stack has the apos-modal-slideable CSS class. Otherwise it defaults to calling self.stackPush() instead, causing the modal to appear normally on top of any modals already open.

# setDepthAttribute($el)

Make the current modal's depth available as an attribute on various elements such as data-apos-modal-instructions. This is needed for reliable Nightwatch testing

# stackPush()

Called for you by self.show(), this method adds the modal to the stack, blacking out the page, preventing unwanted interaction with the page while the modal is active, and stacking on top of any modals already open, if any. This is normal behavior for modals that do not have the transition: 'slide' option set, and fallback behavior if there is no parent modal already on the stack or the parent modal does not have the apos-modal-slideable CSS class.

# indicateCurrentModal(adding)

For ease of browser regression testing, make sure the current modal and its proxies such as $instructions, $modalFilters, etc. all have the data-apos-modal-current attribute, and that nothing else does.

# resizeContentHeight()

Calculates the appropriate modal body height by subtracting header, breadcrumb, and footer heights and an additional 50 pixels from the browser window height. Invoked for you by self.show(). Not needed anymore with use of flexboxes

# refreshBreadcrumb()

Rebuilds the breadcrumb trail of slide titles inside the slideable ancestor of the current slide, or the modal itself if it is a parent of slides. Normally the text of the breadcrumb is simply the title of the corresponding slide modal. If the field option is set, the slide is assumed to be either the modal for editing an array schema field (see apostrophe-schemas) or a modal related to editing one entry in that field. For the former, the title is set to field.label. For the latter, the title is set to the value of the field.titleField property of the array element indicated by the active property of the slide for the array (the previous slide).

TODO: this is dodgy separation of concerns. Where possible the code that pokes into the implementation of the array modal should be replaced by suitable methods that could also be implemented in other places where a similar behavior is desired.

# beforeShow(callback)

This method is provided as your opportunity to modify the DOM via self.$el and add your own event handlers before the modal appears. By default it does nothing, however if you are extending a subclass such as apostrophe-pieces-editor-modal that provides its own version, be sure to invoke the original version before or after yours.

# afterShow()

Called after the modal is visible. Normally you should use beforeShow to do your work behind the scenes, but perhaps you need to call self.$el.width(), which only works properly on visible elements. There is no callback because the modal has no more work to do after yours.

# save(callback)

Save the modal. Prevents simultaneous saves, displays a busy indicator, saves all views if any and then invokes saveContent to do the actual saving of data. If there is no error the modal is hidden (dismissed).

The callback is optional. If it is provided any error preventing the save operation will be passed to it.

self.saveContent is invoked to carry out the actual work (e.g. saving to a database via an API route, for instance) and by default does nothing. If self.saveContent delivers an error to its callback, the save operation fails and the modal is not hidden.

# afterHideInternal()

# saveContent(callback)

Override this method to carry out the actual storing of data when a modal is saved.

If you invoke the callback with an error, the modal does not disappear.

Displaying the error to the user is your responsibility.

# afterHide()

Override this method to clean up timers, etc. after the modal or view has been dismissed.

# afterHideInternal()

Reserved for internal implementation use.

# afterHideWrapper()

Invoked after the modal or view has been dismissed. Calls self.afterHideInternal, which invokes the callbacks of self.save or self.cancel when appropriate, and also invokes self.afterHide, an initially empty method for your overriding convenience.

# confirmCancel(callback)

This method is invoked to confirm the user's request to cancel the modal. Currently invokes confirm, which is ugly. However confirmCancel is async so this can be replaced with a more attractive implementation.

# getBeforeUnloadText()

Returns text to be displayed by the browser in the event the user attempts to leave the page without saving or cancelling the modal, if self.unsavedChanges is truthy.

Note that some browsers now display a generic message in this case in order to discourage misleading wording.

By default the label option passed when creating the modal is used to customize the text.

# getConfirmCancelText()

Returns the text to be displayed to the user when they attempt to cancel the modal, if self.unsavedChanges is truthy.

By default the label option passed when creating the modal is used to customize the text.

# beforeCancel(callback)

Override this method to alter the behavior when the modal is dismissed by clicking the cancel/done button, pressing escape or clicking outside the modal.

You can prevent the modal from disappearing by invoking the callback with an error. The error is not displayed; doing so is your responsibility if you wish to.

You must invoke the callback, with or without an error.

# cancel(callback)

Cancels the modal, dismissing it without invoking save.

Currently this method assumes you wish to close the top modal (or most recent slide of the top modal) and does not actually check to make sure self is that modal. Generally speaking modals that are lower in the stack should not attempt to interfere when the user is working with a new modal on top of the stack.

If the modal has views, their cancel methods are also invoked first.

If the confirmCancel or beforeCancel method invokes its callback with an error the modal is not closed.

self.afterHide is invoked.

# afterHideInternal()

# hide()

Hides (dismisses) the modal, sliding out or popping off the stack as appropriate. Invokes for you when the user saves or cancels. If the modal is a view, nothing happens by default. The hide() methods of any views within the modal are also called.

# slideOut()

Reverses the slide transition, revealing the previous slide. Invoked for you when the user saves or cancels a slide.

# stackPop()

Pops this modal off the stack. Assumes the modal is a stacked modal and not a slide. Called for you when the modal is saved or cancelled.

# overrideFormSubmission()

Prevents the enter key from inadvertently submitting a form the old-fashioned way. Invoked for you by afterConstruct.

# applyBlackout()

Applies a blackout div with the apos-modal-blackout CSS class to hide the content of the page (with partial opacity) and prevent unwanted interactions with the page while the modal is active. Blackouts are also applied to modals higher in the stack for the same reason. The top-level blackout adjusts its height at regular intervals so that it always adequately covers the document while the modal is active. Invoked for you when a new stacked modal is added.

# focusFirstFormElement()

Gives the focus to the first form element in the modal. Invoked for you when a modal is displayed. TODO: should respect tabindex if present.

# getViews()

Returns an array of views nested within the modal.