egroupware_official/api/js/etemplate/Expose/ExposeMixin.ts
nathan b9d145a40b Link fixes
- Fix actual Expose didn't work on links themselves
- Adjust delete button / context menu caption to match file/link
2022-05-12 10:08:59 -06:00

924 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* EGroupware eTemplate2 - Mixin to add expose view of media and a gallery view
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Hadi Nategh <hn[at]egroupware.org>
* @author Nathan Gray <ng[at]egroupware.org>
*/
// Don't import this more than once
import "../../../../node_modules/blueimp-gallery/js/blueimp-gallery.min";
import {html, LitElement, render} from "@lion/core";
import {et2_nextmatch} from "../et2_extension_nextmatch";
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
import {ET2_DATAVIEW_STEPSIZE} from "../et2_dataview_controller";
import {egw, egw_get_file_editor_prefered_mimes} from "../../jsapi/egw_global";
// Minimum data to qualify as an image and not cause errors
const IMAGE_DEFAULT = {
title: egw.lang('loading'),
href: '',
type: 'image/png',
thumbnail: '',
loading: true
};
// For filtering to only show things we can handle
const MIME_REGEX = (navigator.userAgent.match(/(MSIE|Trident)/)) ?
// IE only supports video/mp4 mime type
new RegExp(/(video\/mp4)|(image\/:*(?!tif|x-xcf|pdf))|(audio\/:*)/, 'ig') :
new RegExp(/(video\/(mp4|ogg|webm))|(image\/:*(?!tif|x-xcf|pdf))|(audio\/:*)/, 'ig');
const MIME_AUDIO_REGEX = new RegExp(/(audio\/:*)/, 'ig');
// open office document mime type currently supported by webodf editor
const MIME_ODF_REGEX = new RegExp(/application\/vnd\.oasis\.opendocument\.text/);
type Constructor<T> = { new(...args : any[]) : T };
/**
* Interface used to determine if widget can expose
*/
export interface ExposeValue
{
path : any;
mime : string,
download_url? : string
// File modification time
mtime? : number
}
/**
* Data to show a single slide
*/
export interface MediaValue
{
// Label for the image, shown in top left
title? : string,
// URL to the large version of the image, or full version of file
href : string,
// Mime type
type : string,
// Smaller image (api/thumbnail.php) to show in indicator
thumbnail? : string,
// Url to download the file
download_href? : string
}
export function ExposeMixin<B extends Constructor<LitElement>>(superclass : B)
{
return class extends superclass
{
static get properties()
{
return {
...super.properties,
/**
* Function to extract an image list
*
* "Normally" we'll try to pull a list of images from the nextmatch or show just the current widget,
* but if you know better you can provide a method to get the list.
*/
mediaContentFunction: {type: Function},
}
}
// @ts-ignore
private _gallery : blueimp.Gallery;
private __mediaContentFunction : Function | null;
constructor(...args : any[])
{
super(...args);
// bind handler context to instance
const handlers = [
"expose_onclick",
"expose_onopen",
"expose_onopened",
"expose_onslide",
"expose_onslideend",
"expose_onslidecomplete",
"expose_onclose",
"expose_onclosed"
];
for(let key of handlers)
{
this[key] = (<Function><unknown>this[key]).bind(this);
}
}
connectedCallback()
{
super.connectedCallback();
if(document.body.querySelector('#blueimp-gallery') == null)
{
// Create Gallery DOM structure
render(this._galleryTemplate(), document.body);
}
}
disconnectedCallback()
{
super.disconnectedCallback();
}
/**
* Get the info needed to determine if this widget's value allows it to participate in expose
* It needs to have a path, and we use mime to determine if it can expose
*
* It's also passed to getMedia() when we're not reading from a nextmatch
*
* @returns {ExposeValue}
*/
get exposeValue() : ExposeValue
{
//@ts-ignore value might not exist
return this.value || null;
}
/**
* Get the info needed to show the given value as slide(s)
*
* _value is (usually?) pulled from egw.dataGetUIDdata()
*
* Override this
*/
getMedia(_value) : MediaValue[]
{
let mediaContent = [];
if(_value)
{
mediaContent = [{
title: _value.label,
href: _value.download_url ? this._processUrl(_value.download_url) : this._processUrl(_value.path),
type: _value.mime || (_value.type ? _value.type + "/*" : "")
}];
if(this.isExposable())
{
mediaContent[0].thumbnail = _value.thumbnail ? this._processUrl(_value.thumbnail) : mediaContent[0].href;
}
else
{
let fe = egw_get_file_editor_prefered_mimes(_value.mime);
if(fe && fe.mime[_value.mime] && fe.mime[_value.mime].favIconUrl)
{
mediaContent[0].thumbnail = fe.mime[_value.mime].favIconUrl;
}
}
}
return mediaContent;
}
protected _processUrl(url)
{
let base_url = egw.webserverUrl.match(/^\/ig/) ? egw(window).window.location.origin + egw.webserverUrl + '/' : egw.webserverUrl + '/';
if(base_url && base_url != '/' && url.indexOf(base_url) != 0)
{
url = base_url + url;
}
return url;
}
/**
* Handle changes that have to happen based on changes to properties
*
*/
requestUpdate(name?, oldValue?, options?)
{
super.requestUpdate(name, oldValue, options);
// if there's a value change, (de)bind the gallery
if(name === "value")
{
this._bindGallery();
}
}
/**
* Binds a click handler so if the user clicks, we'll initialize & show the gallery
*
* @protected
*/
protected _bindGallery()
{
// If the media type is not supported do not bind the click handler
if(!this.isExposable())
{
this.classList.remove("et2_clickable");
if(this._gallery)
{
this._gallery.close();
}
return;
}
if(!this._gallery)
{
this.classList.add("et2_clickable");
// Normal click handler will handle it
}
}
public isExposable() : boolean
{
if(!this.exposeValue || typeof this.exposeValue.mime !== "string")
{
return false
}
if(this.exposeValue.mime.match(MIME_REGEX) || this.exposeValue.mime.match(MIME_AUDIO_REGEX))
{
return true;
}
return false;
}
/**
* Just override the normal click handler
*
* @param {MouseEvent} _ev
* @returns {boolean}
*/
_handleClick(_ev : MouseEvent) : boolean
{
if((!this.isExposable() || this.expose_onclick(_ev)) && typeof super._handleClick === "function")
{
return super._handleClick(_ev);
}
return false;
}
get expose_options()
{
return {
// The Id, element or querySelector of the gallery widget:
container: '#blueimp-gallery',
// The tag name, Id, element or querySelector of the slides container:
slidesContainer: 'div',
// The tag name, Id, element or querySelector of the title element:
titleElement: 'h3',
// The class to add when the gallery is visible:
displayClass: 'blueimp-gallery-display',
// The class to add when the gallery controls are visible:
controlsClass: 'blueimp-gallery-controls',
// The class to add when the gallery only displays one element:
singleClass: 'blueimp-gallery-single',
// The class to add when the left edge has been reached:
leftEdgeClass: 'blueimp-gallery-left',
// The class to add when the right edge has been reached:
rightEdgeClass: 'blueimp-gallery-right',
// The class to add when the automatic slideshow is active:
playingClass: 'blueimp-gallery-playing',
// The class for all slides:
slideClass: 'slide',
// The slide class for loading elements:
slideLoadingClass: '',
// The slide class for elements that failed to load:
slideErrorClass: 'slide-error',
// The class for the content element loaded into each slide:
slideContentClass: 'slide-content',
// The class for the "toggle" control:
toggleClass: 'toggle',
// The class for the "prev" control:
prevClass: 'prev',
// The class for the "next" control:
nextClass: 'next',
// The class for the "close" control:
closeClass: 'close',
// The class for the "play-pause" toggle control:
playPauseClass: 'play-pause',
// The class to add for fullscreen button option
fullscreenClass: 'fullscreen',
// The list object property (or data attribute) with the object type:
typeProperty: 'type',
// The list object property (or data attribute) with the object title:
titleProperty: 'title',
// The list object property (or data attribute) with the object URL:
urlProperty: 'href',
// The gallery listens for transitionend events before triggering the
// opened and closed events, unless the following option is set to false:
displayTransition: true,
// Defines if the gallery slides are cleared from the gallery modal,
// or reused for the next gallery initialization:
clearSlides: true,
// Defines if images should be stretched to fill the available space,
// while maintaining their aspect ratio (will only be enabled for browsers
// supporting background-size="contain", which excludes IE < 9).
// Set to "cover", to make images cover all available space (requires
// support for background-size="cover", which excludes IE < 9):
stretchImages: true,
// Toggle the controls on pressing the Return key:
toggleControlsOnReturn: true,
// Toggle the automatic slideshow interval on pressing the Space key:
toggleSlideshowOnSpace: true,
// Navigate the gallery by pressing left and right on the keyboard:
enableKeyboardNavigation: true,
// Close the gallery on pressing the ESC key:
closeOnEscape: true,
// Close the gallery when clicking on an empty slide area:
closeOnSlideClick: false,
// Close the gallery by swiping up or down:
closeOnSwipeUpOrDown: true,
// Emulate touch events on mouse-pointer devices such as desktop browsers:
emulateTouchEvents: true,
// Stop touch events from bubbling up to ancestor elements of the Gallery:
stopTouchEventsPropagation: false,
// Hide the page scrollbars:
hidePageScrollbars: true,
// Stops any touches on the container from scrolling the page:
disableScroll: true,
// Carousel mode (shortcut for carousel specific options):
carousel: true,
// Allow continuous navigation, moving from last to first
// and from first to last slide:
continuous: false,
// Remove elements outside of the preload range from the DOM:
unloadElements: true,
// Start with the automatic slideshow:
startSlideshow: false,
// Delay in milliseconds between slides for the automatic slideshow:
slideshowInterval: 3000,
// The starting index as integer.
// Can also be an object of the given list,
// or an equal object with the same url property:
index: 0,
// The number of elements to load around the current index:
preloadRange: 2,
// The transition speed between slide changes in milliseconds:
transitionSpeed: 400,
//Hide controls when the slideshow is playing
hideControlsOnSlideshow: true,
//Request fullscreen on slide show
toggleFullscreenOnSlideShow: true,
// The transition speed for automatic slide changes, set to an integer
// greater 0 to override the default transition speed:
slideshowTransitionSpeed: undefined,
// The tag name, Id, element or querySelector of the indicator container:
indicatorContainer: 'ol',
// The class for the active indicator:
activeIndicatorClass: 'active',
// The list object property (or data attribute) with the thumbnail URL,
// used as alternative to a thumbnail child element:
thumbnailProperty: 'thumbnail',
// Defines if the gallery indicators should display a thumbnail:
thumbnailIndicators: true,
//thumbnail with image tag
thumbnailWithImgTag: true,
// Callback function executed when the Gallery is initialized.
// Is called with the gallery instance as "this" object:
onopen: this.expose_onopen,
// Callback function executed when the Gallery has been initialized
// and the initialization transition has been completed.
// Is called with the gallery instance as "this" object:
onopened: this.expose_onopened,
// Callback function executed on slide change.
// Is called with the gallery instance as "this" object and the
// current index and slide as arguments:
onslide: this.expose_onslide,
// Callback function executed after the slide change transition.
// Is called with the gallery instance as "this" object and the
// current index and slide as arguments:
onslideend: this.expose_onslideend,
//// Callback function executed on slide content load.
// Is called with the gallery instance as "this" object and the
// slide index and slide element as arguments:
onslidecomplete: this.expose_onslidecomplete,
//// Callback function executed when the Gallery is about to be closed.
// Is called with the gallery instance as "this" object:
onclose: this.expose_onclose,
// Callback function executed when the Gallery has been closed
// and the closing transition has been completed.
// Is called with the gallery instance as "this" object:
onclosed: this.expose_onclosed
}
}
protected _galleryTemplate()
{
return html`
<div id="blueimp-gallery" class="blueimp-gallery">
<div class="slides"></div>
<h3 class="title"></h3>
<a class="prev"></a>
<a class="next"></a>
<a title="' + egw().lang('Close') + '" class="close">×</a><a
title="' + egw().lang('Play/Pause') + '" class="play-pause"></a><a
title="' + egw().lang('Fullscreen') + '" class="fullscreen"></a><a
title="' + egw().lang('Save') + '" class="download"></a>
<ol class="indicator"></ol>
</div>
`;
}
/**
* See if the current widget is in a nextmatch, as this allows us to display
* thumbnails underneath
*
* @param {et2_IExposable} widget
* @returns {et2_nextmatch | null}
*/
protected find_nextmatch(widget)
{
let current = widget;
let nextmatch = null;
while(nextmatch == null && current)
{
current = current.getParent();
if(current && typeof current != 'undefined' && current.instanceOf(et2_nextmatch))
{
nextmatch = current;
}
}
// No nextmatch, or nextmatch not quite ready
// At the moment only filemanger nm would work
// as gallery, thus we disable other nestmatches
// to build up gallery but filemanager
if(nextmatch == null || nextmatch.controller == null || !nextmatch.dom_id.match(/filemanager/, 'ig'))
{
return null;
}
return nextmatch;
};
private _init_blueimp_gallery(event, _value)
{
// Image list
let mediaContent = [];
// We'll customise default options
let options = this.expose_options;
let nm = this.find_nextmatch(this);
if(typeof this.__mediaContentFunction == "function")
{
this.__mediaContentFunction(this);
}
else if(nm && !this._is_target_indepth(nm, event.target))
{
// Get the row that was clicked, find its index in the list
let current_entry = nm.controller.getRowByNode(event.target);
// But before it goes, we'll pull everything we can
this.read_from_nextmatch(nm, mediaContent);
// find current_entry in array and set it's array-index
for(let i = 0; i < mediaContent.length; i++)
{
if('filemanager::' + mediaContent[i].path == current_entry.uid)
{
options.index = i;
break;
}
}
// This will trigger nm to refresh and get just the ones we can handle
// but it might take a while, so do it later - make sure our current
// one is loaded first.
window.setTimeout(function()
{
nm.applyFilters({col_filter: {mime: '/' + MIME_REGEX.source + '/'}});
}, 1);
}
else
{
// Try for all exposable of the same type in the parent widget
try
{
this.getParent().getDOMNode().querySelectorAll(this.localName).forEach((exposable, index) =>
{
if(exposable === this)
{
options.index = mediaContent.length;
}
if(exposable.isExposable())
{
mediaContent.push(...exposable.getMedia(Object.assign({}, IMAGE_DEFAULT, exposable.exposeValue)));
}
});
}
catch(e)
{
// Well, that didn't work. Just the one then.
// @ts-ignore
mediaContent = this.getMedia(_value);
}
// Do not show thumbnail indicator on single expose view
options.thumbnailIndicators = (mediaContent.length > 1);
if(!options.thumbnailIndicators)
{
options.indicatorContainer = 'nope';
}
}
// @ts-ignore
this._gallery = new blueimp.Gallery(mediaContent, options);
}
/**
* Read images out of the data for the nextmatch
*
* @param {et2_nextmatch} nm
* @param {Object[]} images
* @param {number} start_at
* @returns {undefined}
*/
protected read_from_nextmatch(nm, images, start_at?)
{
if(!start_at)
{
start_at = 0;
}
let image_index = start_at;
let stop = Math.max.apply(null, Object.keys(nm.controller._indexMap));
for(let i = start_at; i <= stop; i++)
{
if(!nm.controller._indexMap[i] || !nm.controller._indexMap[i].uid)
{
// Returning instead of using IMAGE_DEFAULT means we stop as
// soon as a hole is found, instead of getting everything that is
// available. The gallery can't fill in the holes.
images[image_index++] = IMAGE_DEFAULT;
continue;
}
let uid = nm.controller._indexMap[i].uid;
if(!uid)
{
continue;
}
let data = egw.dataGetUIDdata(uid);
if(data && data.data && data.data.mime && MIME_REGEX.test(data.data.mime) && !MIME_AUDIO_REGEX.test(data.data.mime))
{
let media = this.getMedia(data.data);
images[image_index++] = Object.assign({}, data.data, media[0]);
}
}
}
/**
* Set a particular index/image in the gallery instead of just appending
* it to the end
*
* @param {integer} index
* @param {Object} image
* @returns {undefined}
*/
protected set_slide(index, image)
{
let active = (index == this._gallery.index);
// Pad with blanks until length is right
while(index > this._gallery.getNumber())
{
this._gallery.add([Object.assign({}, IMAGE_DEFAULT)]);
}
// Don't bother with adding a default, we just did that
if(image.loading)
{
//Add load class if it's really a slide with error
if(this._gallery.slidesContainer.find('[data-index="' + index + '"]').hasClass(this._gallery.options.slideErrorClass))
{
this._gallery.slides[index].classList.add(this._gallery.options.slideLoadingClass)
this._gallery.slides[index].classList.remove(this._gallery.options.slideErrorClass);
}
return;
}
// Remove the loading class if the slide is loaded
else
{
this._gallery.slides[index].classList.remove(this._gallery.options.slideLoadingClass);
}
// Just use add to let gallery create everything it needs
let new_index = this._gallery.num;
this._gallery.add([image]);
// Move it to where we want it.
// Gallery uses arrays and indexes and has several internal variables
// that need to be updated.
//
// list
this._gallery.list[index] = this._gallery.list[new_index];
this._gallery.list.splice(new_index, 1);
// indicators & slides
let dom_nodes = ['indicators', 'slides'];
for(let i in dom_nodes)
{
let var_name = dom_nodes[i];
// Remove old one from DOM
this._gallery[var_name][index].remove();
// Move new one into it's place in gallery
this._gallery[var_name][index] = this._gallery[var_name][new_index];
// Move into place in DOM
let node = this._gallery[var_name][index];
node.setAttribute('data-index', index)
this._gallery.slidesContainer[0].insertBefore(this._gallery.slides[(index + 1)], undefined);
if(active)
{
node.addClass(this._gallery.options.activeIndicatorClass);
}
this._gallery[var_name].splice(new_index, 1);
}
if(active)
{
this._gallery.activeIndicator = this._gallery.indicators[index];
}
// positions
this._gallery.positions[index] = active ? 0 : (index > this._gallery.index ? this._gallery.slideWidth : -this._gallery.slideWidth);
this._gallery.positions.splice(new_index, 1);
// elements - removing will allow to re-do the slide
if(this._gallery.elements[index])
{
delete this._gallery.elements[index];
this._gallery.loadElement(index);
}
// Remove the one we just added
this._gallery.num -= 1;
};
/**
* audio player expose
* @param _value
* @private
*/
private _audio_player(_value)
{
let button = [
{"button_id": 1, "label": egw.lang('close'), id: '1', image: 'cancel', default: true}
];
let mediaContent = this.getMedia(_value)[0];
let dialog = new Et2Dialog();
dialog.transformAttributes({
callback: function(_btn, value)
{
if(_btn == Et2Dialog.OK_BUTTON)
{
}
},
beforeClose: function()
{
},
title: mediaContent.title,
buttons: button,
minWidth: 350,
minHeight: 200,
modal: false,
position: "right bottom,right-50 bottom-10",
value: {
content: {
src: mediaContent.download_href
}
},
resizable: false,
template: egw.webserverUrl + '/api/templates/default/audio_player.xet',
dialogClass: "audio_player"
});
// @ts-ignore
document.body.appendChild(dialog);
}
/**
* Check if clicked target from nm is in depth
*
* @param nm nextmatch widget
* @param target selected target dom node
*
* @return {boolean} returns false if target is not in depth otherwise True
*/
private _is_target_indepth(nm, target?)
{
let res = false;
if(nm)
{
if(!target)
{
// @ts-ignore
let target = this.getDOMNode();
}
let entry = nm.controller.getRowByNode(target);
if(entry && entry.controller.getDepth() > 0)
{
res = true;
}
}
return res;
}
protected expose_onclick(event : MouseEvent)
{
// Do not trigger expose view if one of the operator keys are held
if(event.altKey || event.ctrlKey || event.shiftKey || event.metaKey)
{
return;
}
event.stopImmediatePropagation();
if(this.exposeValue.mime.match(MIME_REGEX) && !this.exposeValue.mime.match(MIME_AUDIO_REGEX))
{
this._init_blueimp_gallery(event, this.exposeValue);
return false;
}
else if(this.exposeValue.mime.match(MIME_AUDIO_REGEX))
{
this._audio_player(this.exposeValue);
return false;
}
return true;
}
protected expose_onopen() {}
protected expose_onopened()
{
// Check to see if we're in a nextmatch, do magic
let nm = this.find_nextmatch(this);
let self = this;
if(nm)
{
// Add scrolling to the indicator list
let total_count = nm.controller._grid.getTotalCount();
if(total_count >= this._gallery.num)
{
let $indicator = this._gallery.container.find('.indicator');
$indicator
.addClass('paginating');
/*
.swipe(function(event, direction, distance)
{
// @ts-ignore
if(direction == jQuery.fn.swipe.directions.LEFT)
{
distance *= -1;
}
// @ts-ignore
else if(direction == jQuery.fn.swipe.directions.RIGHT)
{
// OK.
}
else
{
return;
}
jQuery(this).css('left', Math.min(0, parseInt(jQuery(this).css('left')) - (distance * 30)) + 'px');
});
*/
// Bind the mousewheel handler
$indicator[0].addEventListener('wheel', function(event, _delta)
{
let delta = _delta || event.deltaY / 120;
let g_width = parseInt(getComputedStyle(this._gallery.container[0]).width);
let width = parseInt(getComputedStyle(this._gallery.indicatorContainer[0]).width);
let left = parseInt(getComputedStyle(this._gallery.indicatorContainer[0]).left);
if(delta > 0 && left > g_width / 2)
{
return;
}
//Reload next pictures into the gallery by scrolling on thumbnails
if(delta < 0 && width + left < g_width)
{
let nextIndex = this._gallery.indicatorContainer.find('[title="loading"]')[0];
if(nextIndex)
{
self.expose_onslideend(this._gallery, nextIndex.dataset.index - 1);
}
return;
}
// Move it about 5 indicators
let i_width = parseInt(getComputedStyle(this._gallery.activeIndicator[0]).width);
jQuery($indicator[0]).css('left', left - (-delta * i_width * 5) + 'px');
event.preventDefault();
}.bind(this));
}
}
}
protected expose_onslide(index, slide)
{
//todo
//if (typeof this._super == 'undefined') return;
// First let parent try
let nm = this.find_nextmatch(this);
if(nm)
{
// See if we need to move the indicator
let indicator = this._gallery.container.find('.indicator');
let current = jQuery('.active', indicator).position();
if(current)
{
let width = parseInt(window.getComputedStyle(this._gallery.container[0]).width)
// indicator.animate({left: (width / 2) - current.left}, 10);
}
}
}
protected expose_onslideend(index, slide)
{
// Check to see if we're in a nextmatch, do magic
let nm = this.find_nextmatch(this);
if(nm)
{
// Check to see if we're near the end, or maybe some pagination
// would be good.
let total_count = nm.controller._grid.getTotalCount();
// Already at the end, don't bother
if(index == total_count - 1 || index == 0)
{
return;
}
// Try to determine direction from state of next & previous slides
let direction = 1;
for(let i in this._gallery.elements)
{
// Loading or error
if(this._gallery.elements[i] == 1 || this._gallery.elements[i] == 3 || this._gallery.list[i].loading)
{
direction = i >= index ? 1 : -1;
break;
}
}
if(!this._gallery.list[index + direction] || this._gallery.list[index + direction].loading ||
total_count > this._gallery.getNumber() && index + ET2_DATAVIEW_STEPSIZE > this._gallery.getNumber())
{
// This will get the next batch of rows
let start = Math.max(0, direction > 0 ? index : index - ET2_DATAVIEW_STEPSIZE);
let end = Math.min(total_count - 1, start + ET2_DATAVIEW_STEPSIZE);
nm.controller._gridCallback(start, end);
let images = [];
this.read_from_nextmatch(nm, images, start);
// Gallery always adds to the end, causing problems with pagination
for(let i in images)
{
this.set_slide(i, images[i]);
}
}
}
}
readonly URL_REGEXP = /url\("([^)]+)"\)/;
protected expose_onslidecomplete()
{
const indicators = this._gallery.container.find('ol.indicator')[0].querySelectorAll('li');
indicators.forEach(indicator => {
if (indicator.style.backgroundImage && indicator.style.backgroundImage !== 'none')
{
const img = indicator.ownerDocument.createElement('img');
img.src = indicator.style.backgroundImage.replace(this.URL_REGEXP, '$1');
indicator.appendChild(img);
indicator.style.backgroundImage = 'none';
}
});
}
protected expose_onclose()
{
// Check to see if we're in a nextmatch, remove magic
let nm = this.find_nextmatch(this);
if(nm && !this._is_target_indepth(nm))
{
// Remove scrolling from thumbnails
this._gallery.container.find('.indicator')
.removeClass('paginating')
.off('mousewheel')
.off('swipe');
// Remove applied mime filter
nm.applyFilters({col_filter: {mime: ''}});
}
}
protected expose_onclosed() {}
}
}