/** * USOF Field: Upload */ ! function( $, _undefined ) { const abs = Math.abs; if ( $ush.isUndefined( window.$usof ) ) { return; } /** * @type {{}} Types of previews displayed. */ const PREVIEW_TYPE = { DYNAMIC_VALUE: 'dynamic_value', IMAGE: 'image', TEXT: 'text', }; $usof.field[ 'upload' ] = { init: function( options ) { const self = this; self.parentInit( options ); // Elements self.$container = $( '.usof-upload', self.$row ); self.$preview = $( '.usof-upload-preview', self.$row ); // Private "Variables" self._dynamicLabels = {}; self.hasDynamicValues = $( '.usof-popup', self.$row ).length; self.i18n = {}; self.isMultiple = self.$container.hasClass( 'is_multiple' ); self.placeholder = $( 'input[name="placeholder"]', self.$row ).val(); self.popup = {}; // Get preview type by default self._previewType = self.$container.data( 'preview-type' ); self.$container.removeAttr( 'data-preview-type' ); // Load internationalization var $i18n = $( '.usof-upload-i18n', self.$row ); if ( $i18n.length > 0 ) { self.i18n = $i18n[0].onclick() || {}; $i18n.remove(); } // Bondable events self._events = { removeItem: self._removeItem.bind( self ), selectedInMediaLibrary: self._selectedInMediaLibrary.bind( self ), showMediaLibrary: self._showMediaLibrary.bind( self ), showSelectedInMediaLibrary: self._showSelectedInMediaLibrary.bind( self ), showSelectionWindow: self._showSelectionWindow.bind( self ), }; // Events self.$row // Remove item from selected. .on( 'click', '.ui-icon_close', self._events.removeItem ) // Opens a media uploader. .on( 'click', '.ui-icon_add', self._events.showMediaLibrary ); // Show selection window depending on preview type. if ( ! self.isMultiple ) { self.$row.on( 'click', '.usof-upload-preview-file', self._events.showSelectionWindow ); } // Init Drag & Drop if ( self.isMultiple && self.getPreviewType() === PREVIEW_TYPE.IMAGE ) { self.$body = $( document.body ); self.$window = $( window ); self.$dragShadow = $( '
' ); $.extend( self._events, { dragEnd: self._dragEnd.bind( self ), dragMove: self._dragMove.bind( self ), dragStart: self._dragStart.bind( self ), maybeDragMove: self._maybeDragMove.bind( self ), } ); self.$row .on( 'mousedown', '.usof-upload-preview-file', self._events.dragStart ) // Preventing browser native drag event. .on( 'dragstart', ( e ) => { e.preventDefault() } ); } // If there are dynamic values, then there must be a popup window if ( self.hasDynamicValues ) { self.$variables = $( '.usof-upload-variables', self.$row ); self.$dynamicValue = $( '.usof-upload-dynamic-value', self.$row ); self._events.selectDynamicValue = self.selectDynamicValue.bind( self ); // Events self.$preview.on( 'click', '.usof-upload-preview-file', ( e ) => { if ( self.getPreviewType() === PREVIEW_TYPE.DYNAMIC_VALUE ) { self._showSelectionWindow( e ); } } ); // Set a unique popup id (this is necessary for groups like Dropdown) self.popupId = $ush.uniqid(); $( '[data-popup-id]', self.$row ).attr( 'data-popup-id', self.popupId ); $( '[data-popup-show]', self.$row ).attr( 'data-popup-show', self.popupId ); // Create a new popup self.popup = new $usof.popup( self.popupId, { closeOnEsc: true, // close the popup by pressing Escape closeOnBgClick: true, // close the popup when user clicks on the dark overlay // Fires after first initialization init: function() { /*popup*/this.$container .off( 'click' ) .on( 'click', '[data-dynamic-value]', self._events.selectDynamicValue ) .find( '[data-dynamic-value]' ) .each( ( _, node ) => { const $node = $( node ); if ( $node.data( 'dynamic-label' ) ) { self._dynamicLabels[ $node.data( 'dynamic-value' ) ] = $node.data( 'dynamic-label' ); $node.removeAttr( 'data-dynamic-label' ); } } ); }, // Handler is called before the popup show beforeShow: function() { var that = this; // Set or remove active class $( '[data-dynamic-value]', that.$container ).removeClass( 'active' ); $ush.toString( self.getValue() ) .split( ',' ) .map( ( value ) => { $( `[data-dynamic-value="${value}"]`, that.$container ).addClass( 'active' ); } ); } } ); // Check the initialization of the popup if ( $.isEmptyObject( self.popup ) ) { console.error( 'Failed to initialize popup' ); } } }, /** * Determine whether any of the matched elements are assigned the given class. * Note:Specific method for fixing a bug. * * @param {Node} node The node from document. * @param {String} className The class name one or more separated by a space. * @return {Boolean} True, if there is at least one class, False otherwise. */ _hasClass: function( node, className ) { return $ush.isNode( node ) && ( ' ' + node.className + ' ' ).indexOf( ' ' + className + ' ' ) > - 1; }, /** * Get current preview type. * * @return {String} Returns the current preview type. */ getPreviewType: function() { return this.$container.usMod( 'preview' ); }, /** * Set the preview type. * * @param {String} type The new type. */ setPreviewType: function( type ) { this.$container.usMod( 'preview', $ush.toString( type ) ); }, /** * Show selection window depending on preview type. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _showSelectionWindow: function( e ) { const self = this; // If these are deletions, then we exit if ( self._hasClass( e.target, 'ui-icon_close' ) ) { return; } if ( self.getPreviewType() === PREVIEW_TYPE.DYNAMIC_VALUE ) { $( 'button.for_select_dynamic_value', self.$row ).trigger( 'click' ); } else { self._showMediaLibrary( e ); } }, /** * Show Media Library. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _showMediaLibrary: function( e ) { const self = this; var mediaInstance = self.mediaInstance; // If these are deletions, then we exit if ( self._hasClass( e.target, 'ui-icon_close' ) ) { return; } if ( $ush.isUndefined( mediaInstance ) ) { var mediaSettings = { multiple: self.isMultiple ? 'add' : false, }; if ( self.getPreviewType() === PREVIEW_TYPE.IMAGE ) { mediaSettings.library = { type: 'image' }; } mediaInstance = wp.media( mediaSettings ); mediaInstance.on( 'open', self._events.showSelectedInMediaLibrary ); mediaInstance.on( 'select', self._events.selectedInMediaLibrary ); self.mediaInstance = mediaInstance; } mediaInstance.open(); }, /** * Handler for show selected attachments in Media Library. * * @event handler */ _showSelectedInMediaLibrary: function() { const self = this; const mediaInstance = self.mediaInstance; mediaInstance.state().get( 'selection' ).reset(); $ush.toString( self.getValue() ) .split( ',' ) .map( ( attachmentId, i ) => { if ( ! self.isMultiple && i > 0 ) { return; } mediaInstance.state().get( 'selection' ).add( wp.media.attachment( $ush.parseInt( attachmentId ) ) ); } ); }, /** * Handler for set selected attachments from Media Library. * * @event handler */ _selectedInMediaLibrary: function() { const self = this; const mediaInstance = self.mediaInstance; var value = []; if ( self.isMultiple ) { mediaInstance.state().get( 'selection' ).each( ( attachment ) => { if ( attachment.id ) { value.push( attachment.id ); } } ); } else { const attachment = mediaInstance.state().get( 'selection' ).first(); if ( attachment.attributes.url ) { value = [ attachment.id ]; } } // For WPBakery image filters if ( window.vc_selectedFilters && window.vcAdminNonce ) { $.ajax( { type: 'POST', url: $usof.ajaxUrl, dataType: 'json', data: { action: 'vc_media_editor_add_image', filters: window.vc_selectedFilters, ids: value, _vc_inline: true, _vcnonce: window.vcAdminNonce }, success: ( res ) => { if ( res.data.ids.length ) { self.setValue( res.data.ids.toString() ); } }, complete: () => { window.vc_selectedFilters = _undefined; }, } ); } else { self.setValue( value.toString() ); } }, /** * Remove item from selected. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _removeItem: function( e ) { const self = this; const $previewFile = $( e.target ).closest( '.usof-upload-preview-file' ); const removeValue = $ush.toString( $previewFile.data( 'value' ) ); if ( removeValue ) { var value = $ush.parseInt( removeValue ) !== -1 // -1 installed on demos for placeholder ? $ush.toString( self.getValue() ) : ''; if ( value ) { value = value .split( ',' ) .filter( ( value ) => { return value !== removeValue } ) .toString(); } if ( value ) { self.parentSetValue( value ); } else { self.setValue( '' ); } $previewFile.remove(); } }, /** * Handler for select a dynamic value in a popup. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ selectDynamicValue: function( e ) { const self = this; e.preventDefault(); // Update preview type and set value self.setPreviewType( PREVIEW_TYPE.DYNAMIC_VALUE ); self.setValue( $ush.toString( e.target.dataset['dynamicValue'] ) ); // Hide a popup by its id if ( self.popupId ) { $usof.hidePopup( $ush.toString( self.popupId ) ); } }, /** * Render preview. * * @param {String|undefined} THe URL address or plain text. * @param {String} value The id or dynamic value. * @param {Boolean} removeButton Presence of a remove button. * @param {Numeric} index Preview index in the list if any. */ renderPreview: function( text, value, removeButton, index ) { const self = this; text = String( text ); if ( ! text ) { return } var newPreview = ''; const previewType = self.getPreviewType(); const isPlaceholder = ( text === ':placeholder' ); // Create new preview if ( previewType === PREVIEW_TYPE.DYNAMIC_VALUE ) { newPreview = '' + $ush.toString( self._dynamicLabels[ value ] ) + ''; } else if ( previewType === PREVIEW_TYPE.IMAGE ) { if ( ! isPlaceholder ) { newPreview = ''; } } else { if ( isPlaceholder ) { text = '...'; } newPreview = '' + text.substring( text.lastIndexOf( '/' ) + 1 ) + ''; } if ( removeButton ) { newPreview += '
'; } newPreview = `
${newPreview}
`; // Remove duplicates (placeholders) $( '[data-value="'+ value +'"]', self.$preview ).remove(); // Add the file to the desired position, because adding is asynchronous. var previews = $( '> *', self.$preview ).toArray(); previews.splice( $ush.parseInt( index ), 0, newPreview ); self.$preview.append( previews ); self.$preview.toggleClass( 'hidden', ! previews.length ); }, /** * Set the value. * * @param {String} value The value. * @param {Boolean} quiet The quiet mode. */ setValue: function( value, quiet ) { const self = this; value = $ush.toString( value ); self.parentSetValue( value, quiet ); if ( ! quiet ) { self.trigger( 'change', [ value ] ); } // Clear preview self.$preview.addClass( 'hidden' ).html( '' ); // Set placeholder or clear preview if ( value === '' ) { // Set default preview type self.setPreviewType( self._previewType ); if ( self.placeholder ) { self.renderPreview( self.placeholder ); } return; } if ( value.indexOf( '{{' ) > -1 ) { self.setPreviewType( PREVIEW_TYPE.DYNAMIC_VALUE ); } /** * @param {{}} attachment The attachment object. */ function addAttachment( attachment, index ) { var url = attachment.attributes.url; if ( ! $ush.isUndefined( attachment.attributes.sizes ) ) { var size = self.isMultiple ? 'thumbnail' : 'medium'; if ( $ush.isUndefined( attachment.attributes.sizes[ size ] ) ) { size = 'full'; } url = attachment.attributes.sizes[ size ].url; } self.renderPreview( url, attachment.id, /* removeButton */true, index ); } if ( self.isDynamicVariable( value ) ) { self.renderPreview( value, /* dynamic-value */value, /* removeButton */true ); } else if ( /\.(gif|jpe?g|tiff?|png|webp|bmp)$/i.test( value ) ) { self.renderPreview( value, /* attachmentId */-1, /* removeButton */true ); } else { var queryArgs = { // gets an array of positive values, without zero include: value.split( ',' ).map( $ush.parseInt ).filter( $ush.parseInt ), posts_per_page: -1, }; // Set placeholders queryArgs.include.map( ( attachmentId, index ) => { self.renderPreview( ':placeholder', attachmentId, /* removeButton */true, index ); } ); // Get all attachments in one request wp.media.query( queryArgs ).more().then( () => { queryArgs.include.map( ( value, index ) => { var attachment = wp.media.attachment( $ush.parseInt( value ) ); if ( ! $ush.isUndefined( attachment.attributes.url ) ) { addAttachment( attachment, index ); } else { // Loading missing data via ajax attachment.fetch( { success: addAttachment.bind( null, attachment, index ) } ); } } ); } ); } } }; // Drag & Drop functionality $.extend( $usof.field[ 'upload' ], { /** * Determines whether the specified e is in blind spot. * * @param @param {Event} e The Event interface represents an event which takes place in the DOM. * @return {Boolean} True if the specified e is in blind spot, False otherwise. */ _isInBlindSpot: function( e ) { return ( abs( e.pageX - this.blindSpot[0] ) <= 20 && abs( e.pageY - this.blindSpot[1] ) <= 20 ); }, /** * Handler when the user starts dragging a node. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _dragStart: function( e ) { const self = this; // Prevent drag event start when clicked on delete icon inside image element if ( self._hasClass( e.target, 'ui-icon_close' ) ) { return; } e.stopPropagation(); self.$draggedElm = $( e.target ).closest( '.usof-upload-preview-file' ); self.detached = false; self.blindSpot = [ e.pageX, e.pageY ].map( $ush.parseInt ); self.elmPointerOffset = [ e.pageX, e.pageY ].map( $ush.parseInt ); self.$body.on( 'mousemove', self._events.maybeDragMove ); self.$window.on( 'mouseup', self._events.dragEnd ); }, /** * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _maybeDragMove: function( e ) { const self = this; e.stopPropagation(); if ( self._isInBlindSpot( e ) ) { return; } self.$body.off( 'mousemove', self._events.maybeDragMove ); self._detach(); self.$body.on( 'mousemove', self._events.dragMove ); }, /** * Handler for detaching drag. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _detach: function( e ) { const self = this; const $draggedElm = self.$draggedElm; const offset = $draggedElm.offset(); self.elmPointerOffset[0] -= offset.left; self.elmPointerOffset[1] -= offset.top; self.$dragShadow.css( { width: $draggedElm.outerWidth(), height: $draggedElm.outerHeight() } ).insertBefore( $draggedElm ); $draggedElm.css( { position: 'absolute', 'pointer-events': 'none', zIndex: 10000, width: $draggedElm.width(), height: $draggedElm.height() } ).css( offset ).appendTo( self.$body ); self.detached = true; }, /** * Handler for drag operation completion. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _dragMove: function( e ) { const self = this; e.stopPropagation(); self.$draggedElm.css( { left: e.pageX - self.elmPointerOffset[0], top: e.pageY - self.elmPointerOffset[1] } ); if ( self._isInBlindSpot( e ) ) { return; } var node = e.target; // Checking two levels up for ( var level = 0; level <= 2; level++, node = node.parentNode ) { if ( self._hasClass( node, 'usof-dragshadow' ) ) { return; } if ( self._hasClass( node, 'usof-upload-preview-file' ) ) { // Dropping element before or after sortables based on their relative position in DOM var nextNode = node.previousSibling, shadowAtLeft = false; while ( nextNode ) { if ( nextNode == this.$dragShadow[0] ) { shadowAtLeft = true; break; } nextNode = nextNode.previousSibling; } self.$dragShadow[ shadowAtLeft ? 'insertAfter' : 'insertBefore' ]( node ); self.blindSpot = [ e.pageX, e.pageY ].map( $ush.parseInt ); // dragDrop break; } } }, /** * Handler to complete the drag & drop operation. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _dragEnd: function( e ) { const self = this; self.$window.off( 'mouseup', self._events.dragEnd ); self.$body .off( 'mousemove', self._events.maybeDragMove ) .off( 'mousemove', self._events.dragMove ); if ( ! self.detached ) { return; } self.$draggedElm.removeAttr( 'style' ).insertBefore( self.$dragShadow ); self.$dragShadow.detach(); // Saving the new element position var values = []; $( '.usof-upload-preview-file', self.$preview ).each( ( _, node ) => { const value = $ush.toString( node.dataset['value'] ); if ( value ) { values.push( value ); } } ); if ( values.length ) { self.parentSetValue( values.toString() ); } else { self.setValue( '' ) } } } ); }( jQuery );