/** * USOF Field: Advanced Links */ ! function( $, undefined ) { // Private variables that are used only in the context of this function, it is necessary to optimize the code var _window = window, _document = document; var isUndefined = $ush.isUndefined; // Check for is set objects _window.$ush = _window.$ush || {}; // If there is no $usof we do nothing if ( isUndefined( _window.$usof ) ) { return; } /** * @type {RegExp} Regular expression for check related fields. */ const _REGEXP_RELATED_FIELD_NAME_ = /^([A-z\d_\-]+)_value$/; /** * @var {Numeric} Timer ID when entering characters */ var _typingTimer = 0; /** * @type {{}} Field Methods Factory. */ $usof.field[ 'link' ] = { init: function() { var self = this; // Elements self.$container = $( '.usof-link', self.$row ); self.$linkAtts = $( '.usof-link-attributes', self.$row ); self.$valueSelected = $( '.usof-form-input-dynamic-value', self.$row ); self.$inputUrl = $( '.usof-link-input-url', self.$row ); self.$urlSearchResults = $( '.usof-link-search-results', self.$row ); self.$urlSearchNotice = $( '.usof-link-search-message', self.$urlSearchResults ); self.$checkboxes = $( 'input[type="checkbox"]', self.$linkAtts ); // Variables; self.popup = {}; self.relatedFields = {}; self._dynamicLabels = {}; self.hasDynamicValues = $( '.usof-popup', self.$row ).length; // Delay for search requests self._typingDelay = 0.5; self.urlSearchDoingAjax = false; self.urlSearchAjaxQueriesNumber = 0; self.urlSearcAjaxQueryArgs = { action: 'usof_search_items_for_link', _nonce: self.$inputUrl.data( 'nonce' ) }; // Get related fields with checkboxes self.$checkboxes.each( function( _, checkbox ) { var name = checkbox.name, $input = $( 'input[name="'+ name +'_value"]', self.$linkAtts ); if ( $input.length ) { self.relatedFields[ name ] = $input; } } ); /** * @var {{}} Bondable events */ self._events = { changeCheckbox: self._changeCheckbox.bind( self ), removeDynamicValue: self._removeDynamicValue.bind( self ), selectDynamicValue: self._selectDynamicValue.bind( self ), toggleMenu: self._toggleMenu.bind( self ), inputUrlKeyUpDown: self._inputUrlKeyUpDown.bind( self ), toggleUrlSearchResults: self._toggleUrlSearchResults.bind( self ), urlSearchResultsSelect: self._urlSearchResultsSelect.bind( self ), urlSearchResultsScroll: self._urlSearchResultsScroll.bind( self ), }; // If there are dynamic values, then there must be a popup window if ( self.hasDynamicValues ) { // 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( function( _, node ) { var $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 dynamicValue = self.$valueSelected.data( 'value' ); // Sync of dynamic value with popup buttons self._resetPopupButtons(); // reset all buttons in the popup if ( ! isUndefined( dynamicValue ) ) { $( '[data-dynamic-value="' + dynamicValue + '"]', /*popup*/this.$container ).addClass( 'active' ); } } } ); // Check the initialization of the popup if ( $.isEmptyObject( self.popup ) ) { console.error( 'Failed to initialize popup' ); } } // Events self.$container .on( 'click', '.action_toggle_menu', self._events.toggleMenu ) .on( 'input change', 'input[type="text"]', $ush.debounce( self.updateValue.bind( self ), 1 ) ); self.$inputUrl.off() .on( 'keydown keyup', self._events.inputUrlKeyUpDown ) .on( 'focus blur', self._events.toggleUrlSearchResults ); self.$urlSearchResults.off() .on( 'mousedown', '.usof-link-search-results-item', self._events.urlSearchResultsSelect ) .on( 'scroll', self._events.urlSearchResultsScroll ); if ( self.hasDynamicValues ) { self.$container.on( 'click', '.action_remove_dynamic_value', self._events.removeDynamicValue ); } // Events the menus self.$linkAtts .on( 'change', '.usof-checkbox', self._events.changeCheckbox ); // Set the value for static field (for example on the post page) var value = self.getValue(); if ( value ) { self.setValue( value ); } }, /** * Hide attributes menu. */ hideMenu: function() { this.$container.removeClass( 'show_atts_settings' ); }, /** * Handler for show or hide attributes menu. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _toggleMenu: function( e ) { e.preventDefault(); this.$container.toggleClass( 'show_atts_settings' ); }, /** * Reset all buttons in the popup. * * @param {String|undefined} skipDynamicValue Skip dynamic value on reset. */ _resetPopupButtons: function( skipDynamicValue ) { var self = this; if ( self.popup.$container instanceof $ ) { $( '[data-dynamic-value!="'+ skipDynamicValue +'"].active', self.popup.$container ) .removeClass( 'active' ); } }, /** * Handler for changes to checkbox. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _changeCheckbox: function( e ) { var self = this, name = e.target.name; // Reset the values of the related fields if ( ! e.target.checked && self.relatedFields[ name ] ) { self.relatedFields[ name ].val( '' ); } $( '.action_toggle_menu', self.$row ) .toggleClass( 'has_values', self.$checkboxes.is( ':checked' ) ); self.updateValue(); // update the input value }, /** * 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 ) { e.preventDefault(); var self = this, $target = $( e.target ), value = $target.data( 'dynamic-value' ); // Reset all buttons in the popup self._resetPopupButtons( /* skipDynamicValue */value ); // Select or cancel the current value $target.toggleClass( 'active' ); if ( $target.hasClass( 'active' ) ) { self.setDynamicValue( value ); $usof.hidePopup( $ush.toString( self.popupId ) ); // hide a popup by its id } else { self.removeDynamicValue(); } self.updateValue(); // update the input value }, /** * Handler for remove the dynamic value. * * @event handler * @param {Event} e The Event interface represents an event which takes place in the DOM. */ _removeDynamicValue: function( e ) { e.preventDefault(); e.stopPropagation(); var self = this; self.removeDynamicValue(); self.updateValue(); // update the input value }, /** * Set the dynamic value. * * @param {String} value The dynamic value. */ setDynamicValue: function( value ) { if ( ! value ) { return; } var self = this, title = self._dynamicLabels[ value ] || value; self.$inputUrl .addClass( 'hidden' ) .val( '' ) .trigger( 'change' ); self.$valueSelected .data( 'value', value ) // save value .removeClass( 'hidden' ) .find( '.usof-form-input-dynamic-value-title' ) .attr( 'title', title ) .text( title ); }, /** * Remove a dynamic value. */ removeDynamicValue: function() { var self = this; self.$valueSelected .data( 'value', /* remove */null ) .addClass( 'hidden' ); self.$inputUrl.removeClass( 'hidden' ); }, /** * Show/hide the URL search results list. */ _toggleUrlSearchResults: function( e ) { var self = this, isFocus = ( e.type === 'focus' ), pid = $ush.timeout( function() { self.$urlSearchResults.toggleClass( 'hidden', ! isFocus ); $ush.clearTimeout( pid ); }, ( isFocus ? 0 : 200 ) ); }, /** * Search results list click handler. Sets URL input value to the URL of selected item. */ _urlSearchResultsSelect: function( e ) { var self = this, $target = $( e.currentTarget ), selectedUrl = $target.data( 'url' ) || ''; if ( selectedUrl ) { self.$inputUrl .val( selectedUrl ) .trigger( 'change' ); self._urlSearchResultsClear(); } }, /** * Clear and hide search results list. */ _urlSearchResultsClear: function() { var self = this; $ush.timeout( function() { // Resetting AJAX calls in case any is running at the moment self.urlSearchAjaxQueriesNumber = 0; self.$urlSearchResults .removeClass( 'loading' ) .addClass( 'hidden' ); $( '.usof-link-search-results-item', self.$urlSearchResults ) .remove(); }, 200 ); }, /** * URL input typing handler. Inits the search with small delay after KeyUp event on the input. */ _inputUrlKeyUpDown: function( e ) { if ( ! e.currentTarget.value ) { return; } var self = this; if ( _typingTimer ) { $ush.clearTimeout( _typingTimer ); } _typingTimer = $ush.timeout( function() { self._urlSearchInit.call( self, e ); $ush.clearTimeout( _typingTimer ); }, 1000 ).value; // Enter handler if ( $ush.toLowerCase( e.key ) === 'enter' ) { e.preventDefault(); var $firstSearchResult = $( '.usof-link-search-results-item:first', self.$urlSearchResults ); if ( $firstSearchResult.length ) { $firstSearchResult.trigger( 'mousedown' ); } else { self.$inputUrl .val( '' ) .trigger( 'change' ); } self.$urlSearchResults.addClass( 'hidden' ); } }, /** * URL search initialization. */ _urlSearchInit: function( e ){ var self = this, $input = $( e.currentTarget ), value = $ush.toLowerCase( $input.val() ).trim(), isUrl = true; // Clear previous search results $( '.usof-link-search-results-item', self.$urlSearchResults ).remove(); // Hide notice if it was shown self.$urlSearchNotice.addClass( 'hidden' ); // Do not start search if the value is empty OR starts from one of symbols: #/. if ( value === '' || $.inArray( value.substring( 0, 1 ), [ '#', '/', '.' ] ) !== -1 ) { self._urlSearchResultsClear(); return; } // Do not start search if the value is a valid URL try { var _urlObj; _urlObj = new URL(value); } catch ( _ ) { isUrl = false; } if ( isUrl ) { self._urlSearchResultsClear(); return; } // Clear pagination params, set search value self.urlSearchParams = { search: value, posts_offset: 0, posts_search_completed: 0, terms_offset: 0, terms_search_completed: 0 } self.$urlSearchResults.show(); $( '.usof-link-search-results-item', self.$urlSearchResults ) .remove(); self._urlSearchAjaxCall(); }, /** * URL search iteration on scroll of the search results list. */ _urlSearchResultsScroll: function( e ) { var self = this, $target = $( e.currentTarget ); if ( // if we are done loading posts and terms... // ... or another ajax call is in action right now ... // ... do not proceed with another AJAX call ! self.urlSearchParams.terms_search_completed // if we are done loading posts and terms... && ! self.urlSearchDoingAjax // ... or another ajax call is in action right now - // Trigger next AJAX call when scrolling down at the bottom of the list with results && ( $target.scrollTop() + $target.height() ) >= ( e.currentTarget.scrollHeight - 1 ) ) { self._urlSearchAjaxCall(); } }, /** * URL search AJAX call handler. */ _urlSearchAjaxCall: function() { var self = this, data = $.extend( {}, self.urlSearcAjaxQueryArgs || {}, self.urlSearchParams ); self.$urlSearchResults.addClass( 'loading' ); self.urlSearchDoingAjax = true; // Counting each active AJAX call. There may be several concurrent AJAX cals during typing self.urlSearchAjaxQueriesNumber++; var currentAjaxQueryNumber = self.urlSearchAjaxQueriesNumber; $.post( ajaxurl, data, function( res ) { // If there were several concurrent AJAX calls during typing, applying the one that was called last if ( self.urlSearchAjaxQueriesNumber == currentAjaxQueryNumber ) { // Checking if the response was successful and showing error if it was not if ( ! res.success ) { self.$urlSearchResults.find( '.usof-link-search-results-item' ).remove(); self.$urlSearchNotice .html( res.data.message ) .removeClass( 'hidden' ); // If there are results - adding them to the search results list } else if ( res.data.items && res.data.items.length > 0 ) { // Add results to list $.each( res.data.items, function( index, item ) { if ( $.isPlainObject( item ) ) { var $item = $( '