2022-05-04 19:58:04 +02:00
|
|
|
|
/**
|
|
|
|
|
* 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";
|
2022-05-05 01:31:42 +02:00
|
|
|
|
import {html, LitElement, render} from "@lion/core";
|
2022-05-04 19:58:04 +02:00
|
|
|
|
import {et2_nextmatch} from "../et2_extension_nextmatch";
|
|
|
|
|
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
|
|
|
|
import {ET2_DATAVIEW_STEPSIZE} from "../et2_dataview_controller";
|
|
|
|
|
|
|
|
|
|
// 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
|
2022-05-05 16:51:09 +02:00
|
|
|
|
new RegExp(/(video\/mp4)|(image\/:*(?!tif|x-xcf|pdf))|(audio\/:*)/, 'ig') :
|
|
|
|
|
new RegExp(/(video\/(mp4|ogg|webm))|(image\/:*(?!tif|x-xcf|pdf))|(audio\/:*)/, 'ig');
|
2022-05-04 19:58:04 +02:00
|
|
|
|
|
2022-05-05 16:51:09 +02:00
|
|
|
|
const MIME_AUDIO_REGEX = new RegExp(/(audio\/:*)/, 'ig');
|
2022-05-04 19:58:04 +02:00
|
|
|
|
// 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
|
2022-05-06 23:07:07 +02:00
|
|
|
|
// File modification time
|
|
|
|
|
mtime? : number
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-05-05 16:51:09 +02:00
|
|
|
|
* Data to show a single slide
|
2022-05-04 19:58:04 +02:00
|
|
|
|
*/
|
|
|
|
|
export interface MediaValue
|
|
|
|
|
{
|
2022-05-05 16:51:09 +02:00
|
|
|
|
// Label for the image, shown in top left
|
|
|
|
|
title? : string,
|
|
|
|
|
|
|
|
|
|
// URL to the large version of the image, or full version of file
|
2022-05-04 19:58:04 +02:00
|
|
|
|
href : string,
|
2022-05-05 16:51:09 +02:00
|
|
|
|
|
|
|
|
|
// Mime type
|
2022-05-04 19:58:04 +02:00
|
|
|
|
type : string,
|
2022-05-05 16:51:09 +02:00
|
|
|
|
|
|
|
|
|
// Smaller image (api/thumbnail.php) to show in indicator
|
|
|
|
|
thumbnail? : string,
|
|
|
|
|
|
|
|
|
|
// Url to download the file
|
|
|
|
|
download_href? : string
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ExposeMixin<B extends Constructor<LitElement>>(superclass : B)
|
|
|
|
|
{
|
|
|
|
|
return class extends superclass
|
|
|
|
|
{
|
2022-05-05 21:48:39 +02:00
|
|
|
|
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},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-04 19:58:04 +02:00
|
|
|
|
// @ts-ignore
|
|
|
|
|
private _gallery : blueimp.Gallery;
|
|
|
|
|
|
2022-05-05 21:48:39 +02:00
|
|
|
|
private __mediaContentFunction : Function | null;
|
|
|
|
|
|
2022-05-04 19:58:04 +02:00
|
|
|
|
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();
|
2022-05-05 01:31:42 +02:00
|
|
|
|
|
|
|
|
|
if(document.body.querySelector('#blueimp-gallery') == null)
|
|
|
|
|
{
|
|
|
|
|
// Create Gallery DOM structure
|
|
|
|
|
render(this._galleryTemplate(), document.body);
|
|
|
|
|
}
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2022-05-05 21:48:39 +02:00
|
|
|
|
href: _value.download_url ? this._processUrl(_value.download_url) : this._processUrl(_value.path),
|
2022-05-04 19:58:04 +02:00
|
|
|
|
type: _value.mime || (_value.type ? _value.type + "/*" : "")
|
|
|
|
|
}];
|
2022-05-05 21:48:39 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
return mediaContent;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 21:48:39 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-04 19:58:04 +02:00
|
|
|
|
/**
|
|
|
|
|
* 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
|
2022-05-05 21:48:39 +02:00
|
|
|
|
if(!this.isExposable())
|
2022-05-04 19:58:04 +02:00
|
|
|
|
{
|
|
|
|
|
this.classList.remove("et2_clickable");
|
|
|
|
|
if(this._gallery)
|
|
|
|
|
{
|
|
|
|
|
this._gallery.close();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!this._gallery)
|
|
|
|
|
{
|
|
|
|
|
this.classList.add("et2_clickable");
|
2022-05-05 01:31:42 +02:00
|
|
|
|
|
|
|
|
|
// Normal click handler will handle it
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 21:48:39 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 01:31:42 +02:00
|
|
|
|
/**
|
|
|
|
|
* Just override the normal click handler
|
|
|
|
|
*
|
|
|
|
|
* @param {MouseEvent} _ev
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
_handleClick(_ev : MouseEvent) : boolean
|
|
|
|
|
{
|
2022-05-05 21:48:39 +02:00
|
|
|
|
if((!this.isExposable() || this.expose_onclick(_ev)) && typeof super._handleClick === "function")
|
|
|
|
|
{
|
|
|
|
|
return super._handleClick(_ev);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2022-05-05 01:31:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-04 19:58:04 +02:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 01:31:42 +02:00
|
|
|
|
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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
2022-05-04 19:58:04 +02:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(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)
|
|
|
|
|
{
|
2022-05-05 01:31:42 +02:00
|
|
|
|
// Image list
|
2022-05-04 19:58:04 +02:00
|
|
|
|
let mediaContent = [];
|
2022-05-05 01:31:42 +02:00
|
|
|
|
|
|
|
|
|
// We'll customise default options
|
|
|
|
|
let options = this.expose_options;
|
|
|
|
|
|
2022-05-04 19:58:04 +02:00
|
|
|
|
let nm = this.find_nextmatch(this);
|
2022-05-05 21:48:39 +02:00
|
|
|
|
if(typeof this.__mediaContentFunction == "function")
|
|
|
|
|
{
|
|
|
|
|
this.__mediaContentFunction(this);
|
|
|
|
|
}
|
|
|
|
|
else if(nm && !this._is_target_indepth(nm, event.target))
|
2022-05-04 19:58:04 +02:00
|
|
|
|
{
|
|
|
|
|
// 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)
|
|
|
|
|
{
|
2022-05-05 01:31:42 +02:00
|
|
|
|
options.index = i;
|
2022-05-04 19:58:04 +02:00
|
|
|
|
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
|
|
|
|
|
{
|
2022-05-05 21:48:39 +02:00
|
|
|
|
// 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 = index;
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
2022-05-04 19:58:04 +02:00
|
|
|
|
// Do not show thumbnail indicator on single expose view
|
2022-05-05 01:31:42 +02:00
|
|
|
|
options.thumbnailIndicators = (mediaContent.length > 1);
|
|
|
|
|
if(!options.thumbnailIndicators)
|
|
|
|
|
{
|
|
|
|
|
options.indicatorContainer = 'nope';
|
|
|
|
|
}
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
2022-05-05 01:31:42 +02:00
|
|
|
|
this._gallery = new blueimp.Gallery(mediaContent, options);
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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, "text": 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"
|
|
|
|
|
});
|
2022-05-05 16:51:09 +02:00
|
|
|
|
// @ts-ignore
|
2022-05-04 19:58:04 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 21:48:39 +02:00
|
|
|
|
event.stopImmediatePropagation();
|
2022-05-04 19:58:04 +02:00
|
|
|
|
|
2022-05-05 16:51:09 +02:00
|
|
|
|
if(this.exposeValue.mime.match(MIME_REGEX) && !this.exposeValue.mime.match(MIME_AUDIO_REGEX))
|
2022-05-04 19:58:04 +02:00
|
|
|
|
{
|
|
|
|
|
this._init_blueimp_gallery(event, this.exposeValue);
|
2022-05-05 21:48:39 +02:00
|
|
|
|
return false;
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
2022-05-05 16:51:09 +02:00
|
|
|
|
else if(this.exposeValue.mime.match(MIME_AUDIO_REGEX))
|
2022-05-04 19:58:04 +02:00
|
|
|
|
{
|
|
|
|
|
this._audio_player(this.exposeValue);
|
2022-05-05 21:48:39 +02:00
|
|
|
|
return false;
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
2022-05-06 23:07:07 +02:00
|
|
|
|
|
2022-05-05 21:48:39 +02:00
|
|
|
|
return true;
|
2022-05-04 19:58:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected expose_onslidecomplete() {}
|
|
|
|
|
|
|
|
|
|
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() {}
|
|
|
|
|
}
|
|
|
|
|
}
|