egroupware/api/js/etemplate/expose.ts
Ralf Becker 68155251fa fixing a couple of jQuery plugin includes
thought jQueryUI-tooltip is still broken and we either need to legacy-load jQuery(UI) or fix the tooltip
2021-07-01 16:33:15 +02:00

655 lines
24 KiB
TypeScript
Raw Permalink 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 - JS object implementing 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]stylite.de>
*/
/*egw:uses
/vendor/bower-asset/jquery/dist/jquery.js;
/api/js/jquery/blueimp/js/blueimp-gallery.min.js;
*/
"use strict";
import {et2_createWidget} from "./et2_core_widget";
import {et2_dialog} from "./et2_widget_dialog";
import {egw, egw_get_file_editor_prefered_mimes} from "../jsapi/egw_global";
import {et2_nextmatch} from "./et2_extension_nextmatch";
import {ET2_DATAVIEW_STEPSIZE} from "./et2_dataview_controller";
import "../jquery/blueimp/js/blueimp-gallery.min.js";
import "../../../vendor/bower-asset/jquery-touchswipe/jquery.touchSwipe.js";
type Constructor<T = {}> = new (...args: any[]) => T;
/**
* This function extends the given widget with blueimp gallery plugin
*
* @param {type} widget
* @returns {widget}
*/
export function expose<TBase extends Constructor>(Base: TBase)
{
"use strict";
// 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\/:*)/) :
new RegExp(/(video\/(mp4|ogg|webm))|(image\/:*(?!tif|x-xcf|pdf))|(audio\/:*)/);
const MIME_AUDIO_REGEX = new RegExp(/(audio\/:*)/);
// open office document mime type currently supported by webodf editor
const MIME_ODF_REGEX = new RegExp(/application\/vnd\.oasis\.opendocument\.text/);
// Only one gallery
var gallery = null;
/**
* 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}
*/
var find_nextmatch = function (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;
};
/**
* Read images out of the data for the nextmatch
*
* @param {et2_nextmatch} nm
* @param {Object[]} images
* @param {number} start_at
* @returns {undefined}
*/
var read_from_nextmatch = function (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++] = jQuery.extend({}, 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}
*/
var set_slide = function (index, image) {
let active = (index == gallery.index);
// Pad with blanks until length is right
while (index > gallery.getNumber()) {
gallery.add([jQuery.extend({}, 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 (gallery.slidesContainer.find('[data-index="' + index + '"]').hasClass(gallery.options.slideErrorClass))
jQuery(gallery.slides[index])
.addClass(gallery.options.slideLoadingClass)
.removeClass(gallery.options.slideErrorClass);
return;
}
// Remove the loading class if the slide is loaded
else {
jQuery(gallery.slides[index]).removeClass(gallery.options.slideLoadingClass);
}
// Just use add to let gallery create everything it needs
let new_index = gallery.num;
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
gallery.list[index] = gallery.list[new_index];
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
jQuery(gallery[var_name][index]).remove();
// Move new one into it's place in gallery
gallery[var_name][index] = gallery[var_name][new_index];
// Move into place in DOM
let node = jQuery(gallery[var_name][index]);
node.attr('data-index', index)
.insertAfter(jQuery("[data-index='" + (index - 1) + "']", node.parent()));
if (active) node.addClass(gallery.options.activeIndicatorClass);
gallery[var_name].splice(new_index, 1);
}
if (active) {
gallery.activeIndicator = jQuery(gallery.indicators[index]);
}
// positions
gallery.positions[index] = active ? 0 : (index > gallery.index ? gallery.slideWidth : -gallery.slideWidth);
gallery.positions.splice(new_index, 1);
// elements - removing will allow to re-do the slide
if (gallery.elements[index]) {
delete gallery.elements[index];
gallery.loadElement(index);
}
// Remove the one we just added
gallery.num -= 1;
};
return class exposable extends Base
{
public mime_regexp: RegExp;
private mime_odf_regex: RegExp;
private expose_options: {
container: string; toggleControlsOnReturn: boolean; closeOnSwipeUpOrDown: boolean; clearSlides: boolean; onopen: any; emulateTouchEvents: boolean; onslideend: (index, slide) => void; rightEdgeClass: string; thumbnailWithImgTag: boolean; onslidecomplete: (index, slide) => void; continuous: boolean; startSlideshow: boolean; fullscreenClass: string; onslide: (index, slide) => void; playingClass: string; slideClass: string; urlProperty: string; closeOnEscape: boolean; singleClass: string; slideErrorClass: string; index: number; preloadRange: number; slideContentClass: string; onclosed: any; hidePageScrollbars: boolean; displayTransition: boolean; indicatorContainer: string; disableScroll: boolean; unloadElements: boolean; nextClass: string; stopTouchEventsPropagation: boolean; transitionSpeed: number; carousel: boolean; titleProperty: string; prevClass: string; typeProperty: string; enableKeyboardNavigation: boolean; slidesContainer: string; closeOnSlideClick: boolean; stretchImages: boolean; onclose: any; onopened: any; playPauseClass: string; thumbnailProperty: string; titleElement: string; slideLoadingClass: string; toggleSlideshowOnSpace: boolean; thumbnailIndicators: boolean; activeIndicatorClass: string; displayClass: string; closeClass: string; slideshowInterval: number; toggleClass: string; hideControlsOnSlideshow: boolean; controlsClass: string; toggleFullscreenOnSlideShow: boolean; leftEdgeClass: string; slideshowTransitionSpeed: undefined
};
constructor(...args: any[])
{
// Call the inherited constructor
super(...args);
this.mime_regexp = MIME_REGEX;
this.mime_audio_regexp = MIME_AUDIO_REGEX;
this.mime_odf_regex = MIME_ODF_REGEX;
let self = this;
this.expose_options = {
// 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: jQuery.proxy(this.expose_onopen, this),
// 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: jQuery.proxy(this.expose_onopened, this),
// Callback function executed on slide change.
// Is called with the gallery instance as "this" object and the
// current index and slide as arguments:
onslide: function (index, slide) {
// Call our onslide method, and include gallery as an attribute
self.expose_onslide.apply(self, [this, index, slide]);
},
// 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: function (index, slide) {
// Call our onslide method, and include gallery as an attribute
self.expose_onslideend.apply(self, [this, index, slide]);
},
//// 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: function (index, slide) {
// Call our onslide method, and include gallery as an attribute
self.expose_onslidecomplete.apply(self, [this, index, slide]);
},
//// Callback function executed when the Gallery is about to be closed.
// Is called with the gallery instance as "this" object:
onclose: jQuery.proxy(this.expose_onclose, this),
// 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: jQuery.proxy(this.expose_onclosed, this)
};
let $body = jQuery('body');
if ($body.find('#blueimp-gallery').length == 0) {
// Gallery Main DIV container
let $expose_node = jQuery(document.createElement('div')).attr({
id: "blueimp-gallery",
class: "blueimp-gallery"
});
// Create Gallery DOM NODE
$expose_node.append('<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>');
// Append the gallery Node to DOM
$body.append($expose_node);
}
}
set_value(_value) {
//todo: not sure if we need that with the new construction
//if (typeof this._super == 'undefined') return;
// @ts-ignore
super.set_value(_value);
// Do not run set value of expose if expose_view is not set
// it causes a wired error on nested image widgets which
// seems the expose is not its child widget
// @ts-ignore
if (!this.options.expose_view) {
return;
}
let fe = egw_get_file_editor_prefered_mimes();
let self = this;
// If the media type is not supported do not bind the click handler
if (!_value || typeof _value.mime != 'string' || (!_value.mime.match(MIME_REGEX, 'ig')
&& (!fe || fe.mime && !fe.mime[_value.mime])) || typeof _value.download_url == 'undefined') {
return;
}
// @ts-ignore
if (typeof this.options.expose_view != 'undefined' && this.options.expose_view) {
// @ts-ignore
jQuery(this.node).on('click', function (event) {
// Do not trigger expose view if one of the operator keys are held
if (!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
if (_value.mime.match(MIME_REGEX, 'ig') && !_value.mime.match(MIME_AUDIO_REGEX,'ig')) {
self._init_blueimp_gallery(event, _value);
}
else if (_value.mime.match(MIME_AUDIO_REGEX,'ig'))
{
self._audio_player(_value);
}
else if (fe && fe.mime && fe.edit && fe.mime[_value.mime]) {
egw.open_link(egw.link('/index.php', {
menuaction: fe.edit.menuaction,
path: _value.path,
cd: 'no' // needed to not reload framework in sharing
}), '', fe.edit_popup);
}
}
event.stopImmediatePropagation();
}).addClass('et2_clickable');
}
}
private _init_blueimp_gallery(event, _value) {
let mediaContent = [];
let nm = find_nextmatch(this);
let current_index = 0;
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
read_from_nextmatch.call(this, 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) {
current_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 {
// @ts-ignore
mediaContent = this.getMedia(_value);
// Do not show thumbnail indicator on single expose view
this.expose_options.thumbnailIndicators = false;
}
this.expose_options.index = current_index;
// @ts-ignore
gallery = blueimp.Gallery(mediaContent, this.expose_options);
}
/**
* 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}
];
// @ts-ignore
let mediaContent = this.getMedia(_value)[0];
et2_createWidget("dialog",{
callback: function(_btn, value){
if (_btn == et2_dialog.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"
});
}
/**
* 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;
}
expose_onopen(event) {}
expose_onopened()
{
// Check to see if we're in a nextmatch, do magic
let nm = find_nextmatch(this);
let self = this;
if (nm) {
// Add scrolling to the indicator list
let total_count = nm.controller._grid.getTotalCount();
if (total_count >= gallery.num) {
let $indicator = gallery.container.find('.indicator');
$indicator.off()
.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 for FF (DOMMousewheel), and other browsers (mousewheel)
$indicator.bind('mousewheel DOMMousewheel', function (event, _delta) {
var delta = _delta || event.originalEvent.wheelDelta / 120;
if (delta > 0 && parseInt(jQuery(this).css('left')) > gallery.container.width() / 2) return;
//Reload next pictures into the gallery by scrolling on thumbnails
if (delta < 0 && jQuery(this).width() + parseInt(jQuery(this).css('left')) < gallery.container.width()) {
var nextIndex = gallery.indicatorContainer.find('[title="loading"]')[0];
if (nextIndex) self.expose_onslideend(gallery, nextIndex.dataset.index - 1);
return;
}
// Move it about 5 indicators
jQuery(this).css('left', parseInt(jQuery(this).css('left')) - (-delta * gallery.activeIndicator.width() * 5) + 'px');
event.preventDefault();
});
}
}
}
/**
* Trigger on slide left/right
*/
expose_onslide(gallery, index, slide)
{
//todo
//if (typeof this._super == 'undefined') return;
// First let parent try
super.expose_onslide(gallery, index, slide);
let nm = find_nextmatch(this);
if (nm) {
// See if we need to move the indicator
let indicator = gallery.container.find('.indicator');
let current = jQuery('.active', indicator).position();
if (current) {
indicator.animate({left: (gallery.container.width() / 2) - current.left}, 10);
}
}
}
expose_onslideend(gallery, index)
{
// Check to see if we're in a nextmatch, do magic
let nm = 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 gallery.elements) {
// Loading or error
if (gallery.elements[i] == 1 || gallery.elements[i] == 3 || gallery.list[i].loading) {
direction = i >= index ? 1 : -1;
break;
}
}
if (!gallery.list[index + direction] || gallery.list[index + direction].loading ||
total_count > gallery.getNumber() && index + ET2_DATAVIEW_STEPSIZE > 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 = [];
read_from_nextmatch.call(this, nm, images, start);
// Gallery always adds to the end, causing problems with pagination
for (let i in images) {
//if(i == index || i < gallery.num) continue;
set_slide(i, images[i]);
//gallery.add([images[i]]);
}
}
}
}
expose_onslidecomplete() {}
expose_onclose()
{
// Check to see if we're in a nextmatch, remove magic
let nm = find_nextmatch(this);
if (nm && !this._is_target_indepth(nm))
{
// Remove scrolling from thumbnails
gallery.container.find('.indicator')
.removeClass('paginating')
.off('mousewheel')
.off('swipe');
// Remove applied mime filter
nm.applyFilters({col_filter: {mime: ''}});
}
}
expose_onclosed() {}
}
}