| /* |
| --- |
| |
| description: Adds functionality to create a gallery out of a list of images |
| |
| authors: Christoph Pojer (@cpojer) |
| |
| license: MIT-style license. |
| |
| requires: |
| core/1.4.5: '*' |
| more/1.4.0.1: [Sortables] |
| |
| provides: FileManager.Gallery |
| |
| ... |
| */ |
| |
| (function() { |
| |
| FileManager.Gallery = new Class({ |
| |
| Extends: FileManager, |
| |
| options: |
| { |
| closeCaptionEditorOnMouseOut: true // TRUE: will save & close caption editor popup dialog when you move the mouse out there |
| }, |
| |
| hooks: { |
| fill: { |
| populate: function() { |
| // now we have a valid 'this.root' and we need that to preprocess the paths specified on populate(). So we have a go! |
| this.startPopulatingTheGallery(); |
| } |
| } |
| }, |
| |
| initialize: function(options) |
| { |
| this.offsets = {y: -72}; |
| this.imgContainerSize = { x: 75, y: 56 }; |
| this.captionImgContainerSize = { x: 250, y: 250 }; |
| this.isSorting = false; |
| this.populate_queue = []; |
| |
| // make sure our 'complete' event does NOT clash with the base class event by simply never allowing it: you CANNOT have options.selectable and gallery mode at the same time! |
| // (If you do, the caller will have a hard time detecting /who/ sent the 'complete' event; the only way would be to inspect the argument list and deduce the 'origin' from there.) |
| options.selectable = false; |
| |
| this.parent(options); |
| |
| this.addEvents({ |
| scroll: function(e, self) { |
| self.show_gallery(); |
| }, |
| |
| show: function(self) { |
| self.gallery.empty(); |
| self.files = {}; |
| |
| self.show_gallery(); |
| self.populate(); |
| }, |
| |
| hide: function(self) { |
| if (!self.keepGalleryData) { |
| self.gallery.empty(); |
| self.files = {}; |
| } |
| else { |
| self.keepGalleryData = false; |
| } |
| self.hideClone(); |
| // self.wrapper.setStyle('display', 'none'); |
| }, |
| |
| modify: function(file, json, mode, self) { |
| // mode is one of (destroy, rename, move, copy): only when mode=copy, does the file remain where it was before! |
| if (mode !== 'copy') |
| { |
| self.erasePicture(file.path); |
| } |
| } |
| }); |
| |
| this.addMenuButton('serialize'); |
| this.galleryContainer = new Element('div', { |
| 'class': 'filemanager-gallery', |
| styles: |
| { |
| 'z-index': this.options.zIndex - 10 |
| } |
| }).inject(this.container); |
| this.gallery = new Element('ul').inject(this.galleryContainer); |
| |
| var timer = null; |
| var self = this; |
| |
| this.input = new Element('textarea', {name: 'imgCaption'}); |
| |
| var imgdiv = new Element('div', {'class': 'img'}); |
| this.wrapper = new Element('div', { |
| 'class': 'filemanager-wrapper', |
| styles: |
| { |
| 'z-index': this.options.zIndex + 750, |
| 'opacity': 0, |
| 'display': 'none' |
| }, |
| tween: {duration: 'short'} |
| }).adopt( |
| new Element('span', {text: this.language.gallery.text}), |
| this.input, |
| imgdiv, |
| new Element('button', {text: this.language.gallery.save}).addEvent('click', function(e) { |
| self.removeClone(e, this); |
| }) |
| ); |
| |
| // stop filemanager window events when you hit enter |
| this.gallery_keyboard_handler = (function(e) |
| { |
| this.diag.log('gallery KEYBOARD handler: key press: ', e); |
| |
| if (this.dialogOpen) return; |
| |
| // don't propagate any keyboard code to the filemanager keyboard handler (e.g. TAB) |
| e.stopPropagation(); |
| |
| if (e.key == 'enter') |
| { |
| // deactivated to allow multiple line caption. \n will later be transformed into <br> |
| //self.removeClone(e, this); |
| } |
| }).bind(this); |
| |
| if ((Browser.Engine && (Browser.Engine.trident || Browser.Engine.webkit)) || (Browser.ie || Browser.chrome || Browser.safari)) |
| { |
| this.input.addEvent('keydown', this.gallery_keyboard_handler); |
| // also make sure keyboard navigation actually works in the caption editor pane: catch ENTER on button, etc. from bubbling up to the filemanager handler: |
| this.wrapper.addEvent('keydown', this.gallery_keyboard_handler); |
| } |
| else |
| { |
| this.input.addEvent('keypress', this.gallery_keyboard_handler); |
| // also make sure keyboard navigation actually works in the caption editor pane: catch ENTER on button, etc. from bubbling up to the filemanager handler: |
| this.wrapper.addEvent('keypress', this.gallery_keyboard_handler); |
| } |
| // also make sure keyboard navigation actually works in the caption editor pane: catch ENTER on button, etc. from bubbling up to the filemanager handler: |
| this.wrapper.addEvent('keyup', this.gallery_keyboard_handler); |
| |
| this.wrapper.inject(document.body); |
| |
| var wrapper_pos = this.wrapper.getCoordinates(); |
| var imgdiv_pos = imgdiv.getCoordinates(); |
| this.captionDialogOffsets = { |
| x: Math.round(wrapper_pos.left - imgdiv_pos.left - (imgdiv_pos.width - this.captionImgContainerSize.x) / 2), |
| y: Math.round(wrapper_pos.top - imgdiv_pos.top - (imgdiv_pos.height - this.captionImgContainerSize.y) / 2), |
| h: wrapper_pos.height, |
| w: wrapper_pos.width |
| }; |
| this.droppables.push(this.gallery); |
| |
| this.keepGalleryData = false; |
| this.files = {}; |
| this.animation = {}; |
| |
| this.howto = new Element('div', { |
| 'class': 'howto', |
| styles: |
| { |
| 'z-index': this.options.zIndex + 15 |
| }, |
| text: this.language.gallery.drag |
| }).inject(this.galleryContainer); |
| this.switchButton(); |
| |
| // invoke the parent method directly |
| this.initialShowBase(); |
| |
| // add CAPTION OVERLAY |
| this.captionOverlay = new Overlay({ |
| 'class': 'filemanager-overlay filemanager-overlay-dialog filemanager-overlay-caption', |
| events: { |
| click: (function() { |
| this.removeClone(); |
| this.captionOverlay.hide(); |
| }).bind(this) |
| }, |
| styles: { |
| 'z-index': this.options.zIndex + 11 |
| } |
| }); |
| |
| |
| // -> create SORTABLE list |
| this.sortable = new Sortables(this.gallery, { |
| clone: false, |
| //revert: true, |
| //opacity: 1, |
| onStart: (function() { |
| this.isSorting = true; |
| }).bind(this), |
| onSort: (function(li) { |
| newOrder = this.sortable.serialize((function(element, index) { |
| return this.files[element.retrieve('imageName')]; |
| }).bind(this)); |
| |
| // create new order of images |
| this.files = {}; |
| newOrder.each(function(file) { |
| if (file) { |
| this.files[file.legal_path] = file; |
| } |
| }, this); |
| }).bind(this) |
| }); |
| }, |
| |
| // override the parent's initialShow method: we do not want to jump to the jsGET-stored position again! |
| initialShow: function() { |
| }, |
| |
| show_gallery: function() |
| { |
| this.galleryContainer.setStyles({ |
| opacity: 0, |
| display: 'block' |
| }); |
| |
| this.filemanager.setStyles({ |
| top: '10%', |
| height: '60%' |
| }); |
| |
| this.fitSizes(); |
| |
| var size = this.filemanager.getSize(); |
| var pos = this.filemanager.getPosition(); |
| |
| // determine how many images we can show next to one another: |
| var galWidthComp = 2; // 2px not accounted for in the Width calculation; without these the width slowly grows instead of 'jump' to fit one more/less image horizontally |
| var imgMarginWidth = 4; // 4px unaccounted for in the element width |
| var radiiCorr = 40; // correction for the radii in the FileManager container: looks better that way; must at least be 20 |
| var imgWidth = 84; // derived from current CSS; bad form to do it this way, but this is a good guess when there's no image in there yet. |
| var scrollbarCorr = 20; // correction for optional Y scrollbar when the number of images doesn't fit in two rows anymore. |
| |
| var imgs = this.galleryContainer.getElements('li'); |
| if (imgs && imgs[0]) |
| { |
| // when we HAVE images, derive the width from one of those! |
| imgWidth = imgs[0].getWidth() + imgMarginWidth; |
| } |
| var galWidth = this.galleryContainer.getWidth(); |
| var g_rem = (galWidth - scrollbarCorr) % imgWidth; |
| var img_cnt = Math.floor((size.x - g_rem - scrollbarCorr - radiiCorr) / imgWidth); |
| if (img_cnt < 3) img_cnt = 3; |
| var gw = img_cnt * imgWidth + g_rem + scrollbarCorr; |
| |
| this.galleryContainer.setStyles({ |
| top: pos.y + size.y - 1, |
| left: pos.x + (size.x - gw) / 2, |
| width: gw - 2, |
| opacity: 1 |
| }); |
| |
| this.hideClone(); |
| // this.wrapper.setStyle('display', 'none'); |
| }, |
| |
| // override the parent's allow_DnD method: always allow drag and drop as otherwise we cannot construct our gallery! |
| allow_DnD: function(j, pagesize) { |
| return true; |
| }, |
| |
| show_caption_editor: function(img_el, li_wrapper, file) |
| { |
| var self = this; |
| |
| // -> add overlay |
| $$('filemanager-overlay-caption').addEvent('click', function(e) { |
| this.removeClone(null,li_wrapper); |
| }.bind(this)); |
| this.captionOverlay.show(); |
| |
| var name = file.path; |
| |
| var pos = img_el.getCoordinates(); |
| var oml = img_el.getStyle('margin-left').toInt(); |
| var omt = img_el.getStyle('margin-top').toInt(); |
| pos.left -= oml; |
| pos.top -= omt; |
| |
| var vp = window.getSize(); |
| |
| this.hideClone(); |
| |
| var pic = file.thumb250; |
| var w = file.thumb250_width; |
| var h = file.thumb250_height; |
| |
| if (!pic) |
| { |
| pic = file.icon48; |
| w = 48; |
| h = 48; |
| } |
| |
| // now calculate the scaled image dimensions for this one: |
| var cw = this.captionImgContainerSize.x; |
| var ch = this.captionImgContainerSize.y; |
| var redux; |
| if (w > cw) |
| { |
| redux = cw / w; |
| w *= redux; |
| h *= redux; |
| } |
| if (h > ch) |
| { |
| redux = ch / h; |
| w *= redux; |
| h *= redux; |
| } |
| w = Math.round(w); |
| h = Math.round(h); |
| var ml, mk, mt, mb; |
| ml = Math.round((cw - w) / 2); |
| mr = cw - ml - w; |
| mt = Math.round((ch - h) / 2); |
| mb = ch - mt - h; |
| |
| var nl, nt; |
| //nl = Math.round(pos.left - (w - pos.width) / 2); |
| //nt = pos.top - (h - pos.height); |
| nl = Math.round((vp.x - this.captionDialogOffsets.w) / 2 - this.captionDialogOffsets.x); |
| nt = Math.round((vp.y - this.captionDialogOffsets.h) / 2 - this.captionDialogOffsets.y); |
| |
| this.animation = { |
| from: { |
| width: pos.width, |
| height: pos.height, |
| left: pos.left, |
| top: pos.top, |
| 'margin-left': oml, |
| 'margin-top': omt |
| }, |
| to: { |
| width: w, |
| height: h, |
| left: nl, |
| top: nt, |
| 'margin-left': ml, |
| 'margin-top': mt |
| } |
| }; |
| |
| this.input.removeEvents('blur').addEvent('blur', function() { |
| if (!self.files[name]) |
| return; |
| self.files[name].caption = (this.value || ''); |
| }); |
| |
| // clone image for caption container |
| //li_wrapper.setStyle('opacity', 0); |
| this.clone = img_el.clone(); |
| this.clone.store('file', file).store('parent', li_wrapper).addClass('filemanager-clone').set({ |
| styles: { |
| position: 'relative', |
| width: w, |
| height: h, |
| top: ((260- h) / 2) - 2, |
| left: ((260- w) / 2) - 1, |
| 'z-index': this.options.zIndex + 800 |
| } |
| }).inject(self.wrapper.getChildren('div.img')[0]); |
| |
| // blend in the caption container |
| if (!this.files[name]) |
| return; |
| this.input.set('value', self.files[name].caption || ''); |
| this.wrapper.setStyles({ |
| 'position':'fixed', |
| 'display': 'block', |
| 'left': this.animation.to.left /* -12 */, |
| 'top': this.animation.to.top - this.captionDialogOffsets.y /* -53 */ |
| }).fade('hide').fade(1).get('tween').chain((function() { |
| this.input.focus(); |
| }).bind(this)); |
| }, |
| |
| img_injector: function(file, imgcontainer, li_wrapper) |
| { |
| var pic = file.thumb250; |
| var w = file.thumb250_width; |
| var h = file.thumb250_height; |
| |
| if (!pic) |
| { |
| pic = file.icon48; |
| w = 48; |
| h = 48; |
| } |
| |
| // now calculate the scaled image dimensions for this one: |
| var cw = this.imgContainerSize.x; |
| var ch = this.imgContainerSize.y; |
| var redux; |
| if (w > cw) |
| { |
| redux = cw / w; |
| w *= redux; |
| h *= redux; |
| } |
| if (h > ch) |
| { |
| redux = ch / h; |
| w *= redux; |
| h *= redux; |
| } |
| w = Math.round(w); |
| h = Math.round(h); |
| var ml, mk, mt, mb; |
| ml = Math.round((cw - w) / 2); |
| mr = cw - ml - w; |
| mt = Math.round((ch - h) / 2); |
| mb = ch - mt - h; |
| |
| var self = this; |
| var img = Asset.image(pic, { |
| styles: { |
| width: w, |
| height: h, |
| 'margin-top': mt, |
| 'margin-bottom': mb, |
| 'margin-left': ml, |
| 'margin-right': mr |
| }, |
| onLoad: function() { |
| var img_el = this; |
| li_wrapper.setStyle('background', 'none').addEvent('click', function(e) { |
| if (e) e.stop(); |
| if (!self.isSorting) { |
| self.show_caption_editor(img_el, li_wrapper, file); |
| } |
| self.isSorting = false; |
| }); |
| }, |
| onError: function() { |
| self.diag.log('image asset: error!'); |
| var iconpath = self.assetBasePath + 'Images/Icons/Large/default-error.png'; |
| this.src = iconpath; |
| }, |
| onAbort: function() { |
| self.diag.log('image asset: ABORT!'); |
| var iconpath = self.assetBasePath + 'Images/Icons/Large/default-error.png'; |
| this.src = iconpath; |
| } |
| }); |
| |
| img.inject(imgcontainer); |
| }, |
| |
| onDragComplete: function(el, droppable, caption) { |
| |
| if (typeof caption === 'undefined') { |
| caption = ''; |
| } |
| |
| this.imageadd.fade(0); |
| |
| if (this.howto) { |
| this.howto.destroy(); |
| this.howto = null; |
| } |
| |
| if (!droppable || droppable != this.gallery) |
| return false; |
| |
| var file; |
| if (typeof el === 'string') |
| { |
| var part = el.split('/'); |
| file = { |
| name: part.pop(), |
| path: this.normalize(el), |
| mime: 'unknown/unknown', |
| thumbs_deferred: true |
| }; |
| } |
| else |
| { |
| el.setStyles({left: '', top: ''}); |
| file = el.retrieve('file'); |
| } |
| |
| var name = file.path; |
| |
| // when the item already exists in the gallery, do not add it again: |
| if (this.files[name]) |
| return true; |
| |
| // store & display item in gallery: |
| var self = this; |
| var destroyIcon = Asset.image(this.assetBasePath + 'Images/destroy.png').set({ |
| 'class': 'filemanager-remove', |
| title: this.language.gallery.remove, |
| events: { |
| click: function(e) { |
| if(e) e.stop(); |
| self.erasePicture(name); |
| } |
| } |
| }); |
| // hide on start |
| destroyIcon.setStyle('opacity',0); |
| |
| /** |
| * as 'imgcontainer.getSize() won't deliver the dimensions as set in the CSS, we turn it the other way around: |
| * we set the w/h of the image container here explicitly; the CSS can be used for other bits of styling. |
| */ |
| var imgcontainer = new Element('div', { |
| 'class': 'gallery-image', |
| 'title': file.name, |
| styles: { |
| width: this.imgContainerSize.x, |
| height: this.imgContainerSize.y |
| } |
| }); |
| this.tips.attach(imgcontainer); |
| |
| // STORE the IMAGE PATH in the li FOR the SORTABLE |
| var li = new Element('li').store('imageName', name).adopt( |
| imgcontainer, |
| destroyIcon |
| ).inject(this.gallery); |
| |
| this.files[name] = { |
| legal_path: name, |
| caption: caption, |
| file: file, |
| element: li |
| }; |
| |
| this.showFunctions(destroyIcon,li,1); |
| this.tips.attach(destroyIcon); |
| this.switchButton(); |
| |
| // When the file info is lacking thumbnail info, fetch it by firing a 'detail' request and taking it from there. |
| // Also send our flavour of the 'detail' request when the thumbnail is yet to be generated. |
| if (file.thumbs_deferred) |
| { |
| // request full file info for this one! PLUS direct-access thumbnails! |
| |
| // do NOT set this.Request as this is a parallel request; mutiple ones may be fired when onDragComplete is, for instance, invoked from the array-loop inside populate() |
| |
| var tx_cfg = this.options.mkServerRequestURL(this, 'detail', { |
| directory: this.dirname(file.path), |
| file: file.name, |
| filter: this.options.filter, |
| mode: 'direct' + this.options.detailInfoMode // provide direct links to the thumbnail files |
| }); |
| |
| var req = new FileManager.Request({ |
| url: tx_cfg.url, |
| data: tx_cfg.data, |
| onRequest: function() {}, |
| onSuccess: (function(j) |
| { |
| if (!this.files[name]) |
| return; |
| |
| if (!j || !j.status) { |
| var msg = ('' + j.error).substitute(this.language, /\\?\$\{([^{}]+)\}/g); |
| |
| this.files[name].caption = msg; |
| return; |
| } |
| |
| // the desired behaviour anywhere is NOT identical to that when handling the FileManager 'details' request/event! |
| this.fireEvent('galleryDetails', [j, this]); |
| |
| // We also want to hold onto the data so we can access it later on, |
| // e.g. when returning the gallery collection to the user. |
| |
| // now mix with the previously existing 'file' info: |
| file = Object.merge(file, j); |
| |
| // remove unwanted JSON elements: |
| delete file.thumbs_deferred; |
| delete file.status; |
| delete file.error; |
| delete file.content; |
| |
| if (file.element) |
| { |
| file.element.store('file', file); |
| } |
| |
| // and update the gallery pane: |
| li.store('file', file); |
| |
| //this.onDragComplete(li, droppable); |
| this.files[name].file = file; |
| |
| this.img_injector(file, imgcontainer, li); |
| |
| }).bind(this), |
| onError: (function(text, error) { |
| }).bind(this), |
| onFailure: (function(xmlHttpRequest) { |
| }).bind(this) |
| }, this); |
| |
| this.RequestQueue.addRequest('populate:' + String.uniqueID(), req); |
| req.send(); |
| |
| // while the 'details' request is sent off, keep a 'loader' animation in the spot where the thumbnail/image should end up once we've got that info from the 'details' request |
| } |
| else |
| { |
| // we already have all required information. Go show the image in the gallery pane! |
| this.img_injector(file, imgcontainer, li); |
| } |
| |
| // -> add this list item to the SORTABLE |
| this.sortable.addItems(li); |
| |
| return true; |
| }, |
| |
| removeClone: function(e, target) { |
| if (e) e.stop(); |
| |
| // remove the overlay |
| $$('filemanager-overlay-caption').removeEvents('click'); |
| this.captionOverlay.hide(); |
| |
| if (!this.clone || (e && e.relatedTarget && ([this.clone, this.wrapper].contains(e.relatedTarget) || (e && this.wrapper.contains(e.relatedTarget) && e.relatedTarget != this.wrapper)))) |
| return; |
| if (this.clone.get('morph').timer) |
| return; |
| |
| var file = this.clone.retrieve('file'); |
| if (!file) |
| return; |
| |
| var name = file.path; |
| |
| if (!this.files[name]) |
| return; |
| |
| this.files[name].caption = (this.input.value || ''); |
| |
| self = this; |
| |
| this.wrapper.fade(0).get('tween').chain(function() { |
| this.element.setStyle('display', 'none'); |
| self.clone.destroy(); |
| }); |
| }, |
| |
| hideClone: function() { |
| if (!this.clone) |
| return; |
| |
| this.clone.get('morph').cancel(); |
| var parent = this.clone.retrieve('parent'); |
| if (parent) { |
| //parent.setStyle('opacity', 1); |
| } |
| this.clone.destroy(); |
| this.wrapper.setStyles({ |
| opacity: 0, |
| 'display': 'none' |
| }); |
| }, |
| |
| erasePicture: function(name) { |
| if (this.files[name]) |
| { |
| this.tips.hide(); |
| |
| var self = this; |
| this.files[name].element.set('tween', {duration: 'short'}).removeEvents('click').fade(0).get('tween').chain(function() { |
| this.element.destroy(); |
| self.switchButton(); |
| delete self.files[name]; |
| }); |
| } |
| }, |
| |
| switchButton: function() { |
| if (typeof this.gallery != 'undefined') { |
| var chk = !!this.gallery.getChildren().length; |
| this.menu.getElement('button.filemanager-serialize').set('disabled', !chk)[(chk ? 'remove' : 'add') + 'Class']('disabled'); |
| } |
| }, |
| |
| populate: function(data, path_is_urlencoded /* default = TRUE */) |
| { |
| this.diag.log('GALLERY.populate: ', data, ', is_urlencoded: ', path_is_urlencoded); |
| |
| if (typeof path_is_urlencoded == 'undefined' || path_is_urlencoded === null) |
| path_is_urlencoded = true; // set default to TRUE |
| else |
| path_is_urlencoded = !!path_is_urlencoded; |
| |
| // |
| // WARNING: these items are abs.path encoded and we to convert them to 'legal URL' directory space or the server will reject these on security grounds. |
| // But we don't know the 'legal URL root' path as that is a server-side setting, so we MUST delay the population of the gallery until our first |
| // request has arrived with this desperately needed item. |
| // We wait for the dirscan ('view' request) to complete; actually we wait until the fill() has finished as by that time we'll be sure |
| // to have a valid this.root (or a grandiose server comm failure!) |
| // Until that time, we push all items to populate the gallery with on the populate stack. |
| |
| Object.each(data || {}, function(v, i) { |
| this.diag.log('GALLERY.populate push: index = ', i, ', value = ', v, ', enc: ', (1 * path_is_urlencoded)); |
| if (path_is_urlencoded) |
| { |
| i = this.unescapeRFC3986(i); |
| } |
| this.populate_queue.push({ |
| path: i, |
| caption: v |
| }); |
| }, this); |
| }, |
| |
| startPopulatingTheGallery: function() |
| { |
| if (!this.root) |
| { |
| this.diag.log('### FATAL error in startPopulatingTheGallery(): no valid .root path!'); |
| return; |
| } |
| |
| var count = this.populate_queue.length; |
| if (count) |
| { |
| // we've got work to do, folks! |
| var i; |
| var abs_root = this.normalize('/' + this.root); |
| |
| for (i = 0; i < count; ++i) |
| { |
| // LIFO queue: |
| var item = this.populate_queue.shift(); |
| var path = item.path; |
| |
| // coded so that we support 'legal URL space' items and 'absolute URL path' items at the same time: |
| // when paths start with the root directory, we'll strip that off to make them 'legal URL space' compliant filespecs. |
| if (path.indexOf(abs_root) === 0) |
| { |
| path = path.substr(this.root.length); |
| } |
| if (path) |
| { |
| this.onDragComplete(path, this.gallery, item.caption); |
| } |
| else |
| { |
| this.diag.log('### gallery populate: invalid input (not in legal URL space): ', item, ', root: ', this.root); |
| } |
| } |
| } |
| }, |
| |
| serialize_on_click: function(e) { |
| if (e) e.stop(); |
| |
| var serialized = {}; |
| var metas = {}; |
| var index = 0; |
| Object.each(this.files,function(file) |
| { |
| var path = (this.options.deliverPathAsLegalURL ? file.file.path : this.escapeRFC3986(this.normalize('/' + this.root + file.file.path))); // the absolute URL for the given file, rawURLencoded |
| var caption = (file.caption || ''); |
| serialized[path] = caption; |
| var m = Object.clone(file.file); |
| m['order_no'] = index++; |
| m['caption'] = caption; |
| m['pathRFC3986'] = path; |
| metas[path] = m; |
| }, this); |
| this.keepGalleryData = true; |
| this.hide(e); |
| this.fireEvent('complete', [serialized, metas, this.root, this]); |
| } |
| }); |
| |
| })(); |
| |