primo commit
This commit is contained in:
		
							
								
								
									
										610
									
								
								media/system/js/fields/joomla-field-subform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										610
									
								
								media/system/js/fields/joomla-field-subform.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,610 @@ | ||||
| /** | ||||
|  * @copyright  (C) 2019 Open Source Matters, Inc. <https://www.joomla.org> | ||||
|  * @license    GNU General Public License version 2 or later; see LICENSE.txt | ||||
|  */ | ||||
|  | ||||
| const KEYCODE = { | ||||
|   SPACE: 'Space', | ||||
|   ESC: 'Escape', | ||||
|   ENTER: 'Enter' | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Helper for testing whether a selection modifier is pressed | ||||
|  * @param {Event} event | ||||
|  * | ||||
|  * @returns {boolean|*} | ||||
|  */ | ||||
| function hasModifier(event) { | ||||
|   return event.ctrlKey || event.metaKey || event.shiftKey; | ||||
| } | ||||
| class JoomlaFieldSubform extends HTMLElement { | ||||
|   // Attribute getters | ||||
|   get buttonAdd() { | ||||
|     return this.getAttribute('button-add'); | ||||
|   } | ||||
|   get buttonRemove() { | ||||
|     return this.getAttribute('button-remove'); | ||||
|   } | ||||
|   get buttonMove() { | ||||
|     return this.getAttribute('button-move'); | ||||
|   } | ||||
|   get rowsContainer() { | ||||
|     return this.getAttribute('rows-container'); | ||||
|   } | ||||
|   get repeatableElement() { | ||||
|     return this.getAttribute('repeatable-element'); | ||||
|   } | ||||
|   get minimum() { | ||||
|     return this.getAttribute('minimum'); | ||||
|   } | ||||
|   get maximum() { | ||||
|     return this.getAttribute('maximum'); | ||||
|   } | ||||
|   get name() { | ||||
|     return this.getAttribute('name'); | ||||
|   } | ||||
|   set name(value) { | ||||
|     // Update the template | ||||
|     this.template = this.template.replace(new RegExp(` name="${this.name.replace(/[[\]]/g, '\\$&')}`, 'g'), ` name="${value}`); | ||||
|     this.setAttribute('name', value); | ||||
|   } | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const that = this; | ||||
|  | ||||
|     // Get the rows container | ||||
|     this.containerWithRows = this; | ||||
|     if (this.rowsContainer) { | ||||
|       const allContainers = this.querySelectorAll(this.rowsContainer); | ||||
|  | ||||
|       // Find closest, and exclude nested | ||||
|       Array.from(allContainers).forEach(container => { | ||||
|         if (container.closest('joomla-field-subform') === this) { | ||||
|           this.containerWithRows = container; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Keep track of row index, this is important to avoid a name duplication | ||||
|     // Note: php side should reset the indexes each time, eg: $value = array_values($value); | ||||
|     this.lastRowIndex = this.getRows().length - 1; | ||||
|  | ||||
|     // Template for the repeating group | ||||
|     this.template = ''; | ||||
|  | ||||
|     // Prepare a row template, and find available field names | ||||
|     this.prepareTemplate(); | ||||
|  | ||||
|     // Bind buttons | ||||
|     if (this.buttonAdd || this.buttonRemove) { | ||||
|       this.addEventListener('click', event => { | ||||
|         let btnAdd = null; | ||||
|         let btnRem = null; | ||||
|         if (that.buttonAdd) { | ||||
|           btnAdd = event.target.closest(that.buttonAdd); | ||||
|         } | ||||
|         if (that.buttonRemove) { | ||||
|           btnRem = event.target.closest(that.buttonRemove); | ||||
|         } | ||||
|  | ||||
|         // Check active, with extra check for nested joomla-field-subform | ||||
|         if (btnAdd && btnAdd.closest('joomla-field-subform') === that) { | ||||
|           let row = btnAdd.closest(that.repeatableElement); | ||||
|           row = row && row.closest('joomla-field-subform') === that ? row : null; | ||||
|           that.addRow(row); | ||||
|           event.preventDefault(); | ||||
|         } else if (btnRem && btnRem.closest('joomla-field-subform') === that) { | ||||
|           const row = btnRem.closest(that.repeatableElement); | ||||
|           that.removeRow(row); | ||||
|           event.preventDefault(); | ||||
|         } | ||||
|       }); | ||||
|       this.addEventListener('keydown', event => { | ||||
|         if (event.code !== KEYCODE.SPACE) return; | ||||
|         const isAdd = that.buttonAdd && event.target.matches(that.buttonAdd); | ||||
|         const isRem = that.buttonRemove && event.target.matches(that.buttonRemove); | ||||
|         if ((isAdd || isRem) && event.target.closest('joomla-field-subform') === that) { | ||||
|           let row = event.target.closest(that.repeatableElement); | ||||
|           row = row && row.closest('joomla-field-subform') === that ? row : null; | ||||
|           if (isRem && row) { | ||||
|             that.removeRow(row); | ||||
|           } else if (isAdd) { | ||||
|             that.addRow(row); | ||||
|           } | ||||
|           event.preventDefault(); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Sorting | ||||
|     if (this.buttonMove) { | ||||
|       this.setUpDragSort(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Search for existing rows | ||||
|    * @returns {HTMLElement[]} | ||||
|    */ | ||||
|   getRows() { | ||||
|     const rows = Array.from(this.containerWithRows.children); | ||||
|     const result = []; | ||||
|  | ||||
|     // Filter out the rows | ||||
|     rows.forEach(row => { | ||||
|       if (row.matches(this.repeatableElement)) { | ||||
|         result.push(row); | ||||
|       } | ||||
|     }); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Prepare a row template | ||||
|    */ | ||||
|   prepareTemplate() { | ||||
|     const tmplElement = [].slice.call(this.children).filter(el => el.classList.contains('subform-repeatable-template-section')); | ||||
|     if (tmplElement[0]) { | ||||
|       this.template = tmplElement[0].innerHTML; | ||||
|     } | ||||
|     if (!this.template) { | ||||
|       throw new Error('The row template is required for the subform element to work'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add new row | ||||
|    * @param {HTMLElement} after | ||||
|    * @returns {HTMLElement} | ||||
|    */ | ||||
|   addRow(after) { | ||||
|     // Count how many we already have | ||||
|     const count = this.getRows().length; | ||||
|     if (count >= this.maximum) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     // Make a new row from the template | ||||
|     let tmpEl; | ||||
|     if (this.containerWithRows.nodeName === 'TBODY' || this.containerWithRows.nodeName === 'TABLE') { | ||||
|       tmpEl = document.createElement('tbody'); | ||||
|     } else { | ||||
|       tmpEl = document.createElement('div'); | ||||
|     } | ||||
|     tmpEl.innerHTML = this.template; | ||||
|     const row = tmpEl.children[0]; | ||||
|  | ||||
|     // Add to container | ||||
|     if (after) { | ||||
|       after.parentNode.insertBefore(row, after.nextSibling); | ||||
|     } else { | ||||
|       this.containerWithRows.append(row); | ||||
|     } | ||||
|  | ||||
|     // Add draggable attributes | ||||
|     if (this.buttonMove) { | ||||
|       row.setAttribute('draggable', 'false'); | ||||
|       row.setAttribute('aria-grabbed', 'false'); | ||||
|       row.setAttribute('tabindex', '0'); | ||||
|     } | ||||
|  | ||||
|     // Marker that it is new | ||||
|     row.setAttribute('data-new', '1'); | ||||
|     // Fix names and ids, and reset values | ||||
|     this.fixUniqueAttributes(row, count); | ||||
|  | ||||
|     // Tell about the new row | ||||
|     this.dispatchEvent(new CustomEvent('subform-row-add', { | ||||
|       detail: { | ||||
|         row | ||||
|       }, | ||||
|       bubbles: true | ||||
|     })); | ||||
|     row.dispatchEvent(new CustomEvent('joomla:updated', { | ||||
|       bubbles: true, | ||||
|       cancelable: true | ||||
|     })); | ||||
|     return row; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Remove the row | ||||
|    * @param {HTMLElement} row | ||||
|    */ | ||||
|   removeRow(row) { | ||||
|     // Count how much we have | ||||
|     const count = this.getRows().length; | ||||
|     if (count <= this.minimum) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Tell about the row will be removed | ||||
|     this.dispatchEvent(new CustomEvent('subform-row-remove', { | ||||
|       detail: { | ||||
|         row | ||||
|       }, | ||||
|       bubbles: true | ||||
|     })); | ||||
|     row.dispatchEvent(new CustomEvent('joomla:removed', { | ||||
|       bubbles: true, | ||||
|       cancelable: true | ||||
|     })); | ||||
|     row.parentNode.removeChild(row); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Fix name and id for fields that are in the row | ||||
|    * @param {HTMLElement} row | ||||
|    * @param {Number} count | ||||
|    */ | ||||
|   fixUniqueAttributes(row, count) { | ||||
|     const countTmp = count || 0; | ||||
|     const group = row.getAttribute('data-group'); // current group name | ||||
|     const basename = row.getAttribute('data-base-name'); | ||||
|     const countnew = Math.max(this.lastRowIndex, countTmp); | ||||
|     const groupnew = basename + countnew; // new group name | ||||
|  | ||||
|     this.lastRowIndex = countnew + 1; | ||||
|     row.setAttribute('data-group', groupnew); | ||||
|  | ||||
|     // Fix inputs that have a "name" attribute | ||||
|     let haveName = row.querySelectorAll('[name]'); | ||||
|     const ids = {}; // Collect id for fix checkboxes and radio | ||||
|  | ||||
|     // Filter out nested | ||||
|     haveName = [].slice.call(haveName).filter(el => { | ||||
|       if (el.nodeName === 'JOOMLA-FIELD-SUBFORM') { | ||||
|         // Skip self in .closest() call | ||||
|         return el.parentElement.closest('joomla-field-subform') === this; | ||||
|       } | ||||
|       return el.closest('joomla-field-subform') === this; | ||||
|     }); | ||||
|     haveName.forEach(elem => { | ||||
|       const $el = elem; | ||||
|       const name = $el.getAttribute('name'); | ||||
|       const aria = $el.getAttribute('aria-describedby'); | ||||
|       const id = name.replace(/(\[\]$)/g, '').replace(/(\]\[)/g, '__').replace(/\[/g, '_').replace(/\]/g, ''); // id from name | ||||
|       const nameNew = name.replace(`[${group}][`, `[${groupnew}][`); // New name | ||||
|       let idNew = id.replace(group, groupnew).replace(/\W/g, '_'); // Count new id | ||||
|       let countMulti = 0; // count for multiple radio/checkboxes | ||||
|       const forOldAttr = $el.id; // Fix "for" in the labels | ||||
|  | ||||
|       if ($el.type === 'checkbox' && name.match(/\[\]$/)) { | ||||
|         // <input type="checkbox" name="name[]"> fix | ||||
|         countMulti = ids[id] ? ids[id].length : 0; | ||||
|  | ||||
|         // Set the id for fieldset and group label | ||||
|         if (!countMulti) { | ||||
|           // Look for <fieldset class="checkboxes"></fieldset> or <fieldset><div class="checkboxes"></div></fieldset> | ||||
|           let fieldset = $el.closest('.checkboxes, fieldset'); | ||||
|           // eslint-disable-next-line no-nested-ternary | ||||
|           if (fieldset) { | ||||
|             // eslint-disable-next-line no-nested-ternary | ||||
|             fieldset = fieldset.nodeName === 'FIELDSET' ? fieldset : fieldset.parentElement.nodeName === 'FIELDSET' ? fieldset.parentElement : false; | ||||
|           } | ||||
|           if (fieldset) { | ||||
|             const oldSetId = fieldset.id; | ||||
|             fieldset.id = idNew; | ||||
|             const groupLbl = row.querySelector(`label[for="${oldSetId}"]`); | ||||
|             if (groupLbl) { | ||||
|               groupLbl.setAttribute('for', idNew); | ||||
|               if (groupLbl.id) { | ||||
|                 groupLbl.setAttribute('id', `${idNew}-lbl`); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         idNew += countMulti; | ||||
|       } else if ($el.type === 'radio') { | ||||
|         // <input type="radio"> fix | ||||
|         countMulti = ids[id] ? ids[id].length : 0; | ||||
|  | ||||
|         // Set the id for fieldset and group label | ||||
|         if (!countMulti) { | ||||
|           /** | ||||
|            * Look for one of: | ||||
|            * - <fieldset class="radio"></fieldset> | ||||
|            * - <fieldset><div class="radio"></div></fieldset> | ||||
|            * - <fieldset><div class="switcher"></div></fieldset> | ||||
|            */ | ||||
|           let fieldset = $el.closest('.radio, .switcher, fieldset'); | ||||
|           if (fieldset) { | ||||
|             // eslint-disable-next-line no-nested-ternary | ||||
|             fieldset = fieldset.nodeName === 'FIELDSET' ? fieldset : fieldset.parentElement.nodeName === 'FIELDSET' ? fieldset.parentElement : false; | ||||
|           } | ||||
|           if (fieldset) { | ||||
|             const oldSetId = fieldset.id; | ||||
|             fieldset.id = idNew; | ||||
|             const groupLbl = row.querySelector(`label[for="${oldSetId}"]`); | ||||
|             if (groupLbl) { | ||||
|               groupLbl.setAttribute('for', idNew); | ||||
|               if (groupLbl.id) { | ||||
|                 groupLbl.setAttribute('id', `${idNew}-lbl`); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         idNew += countMulti; | ||||
|       } | ||||
|  | ||||
|       // Cache already used id | ||||
|       if (ids[id]) { | ||||
|         ids[id].push(true); | ||||
|       } else { | ||||
|         ids[id] = [true]; | ||||
|       } | ||||
|  | ||||
|       // Replace the name to new one | ||||
|       $el.name = nameNew; | ||||
|       if ($el.id) { | ||||
|         $el.id = idNew; | ||||
|       } | ||||
|       if (aria) { | ||||
|         $el.setAttribute('aria-describedby', `${nameNew}-desc`); | ||||
|       } | ||||
|  | ||||
|       // Check if there is a label for this input | ||||
|       const lbl = row.querySelector(`label[for="${forOldAttr}"]`); | ||||
|       if (lbl) { | ||||
|         lbl.setAttribute('for', idNew); | ||||
|         if (lbl.id) { | ||||
|           lbl.setAttribute('id', `${idNew}-lbl`); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Use of HTML Drag and Drop API | ||||
|    * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API | ||||
|    * https://www.sitepoint.com/accessible-drag-drop/ | ||||
|    */ | ||||
|   setUpDragSort() { | ||||
|     const that = this; // Self reference | ||||
|     let item = null; // Storing the selected item | ||||
|     let touched = false; // We have a touch events | ||||
|  | ||||
|     // Find all existing rows and add draggable attributes | ||||
|     this.getRows().forEach(row => { | ||||
|       row.setAttribute('draggable', 'false'); | ||||
|       row.setAttribute('aria-grabbed', 'false'); | ||||
|       row.setAttribute('tabindex', '0'); | ||||
|     }); | ||||
|  | ||||
|     // Helper method to test whether Handler was clicked | ||||
|     function getMoveHandler(element) { | ||||
|       return !element.form // This need to test whether the element is :input | ||||
|       && element.matches(that.buttonMove) ? element : element.closest(that.buttonMove); | ||||
|     } | ||||
|  | ||||
|     // Helper method to move row to selected position | ||||
|     function switchRowPositions(src, dest) { | ||||
|       let isRowBefore = false; | ||||
|       if (src.parentNode === dest.parentNode) { | ||||
|         for (let cur = src; cur; cur = cur.previousSibling) { | ||||
|           if (cur === dest) { | ||||
|             isRowBefore = true; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (isRowBefore) { | ||||
|         dest.parentNode.insertBefore(src, dest); | ||||
|       } else { | ||||
|         dest.parentNode.insertBefore(src, dest.nextSibling); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      *  Touch interaction: | ||||
|      * | ||||
|      *  - a touch of "move button" marks a row draggable / "selected", | ||||
|      *     or deselect previous selected | ||||
|      * | ||||
|      *  - a touch of "move button" in the destination row will move | ||||
|      *     a selected row to a new position | ||||
|      */ | ||||
|     this.addEventListener('touchstart', event => { | ||||
|       touched = true; | ||||
|  | ||||
|       // Check for .move button | ||||
|       const handler = getMoveHandler(event.target); | ||||
|       const row = handler ? handler.closest(that.repeatableElement) : null; | ||||
|       if (!row || row.closest('joomla-field-subform') !== that) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // First selection | ||||
|       if (!item) { | ||||
|         row.setAttribute('draggable', 'true'); | ||||
|         row.setAttribute('aria-grabbed', 'true'); | ||||
|         item = row; | ||||
|       } else { | ||||
|         // Second selection | ||||
|         // Move to selected position | ||||
|         if (row !== item) { | ||||
|           switchRowPositions(item, row); | ||||
|         } | ||||
|         item.setAttribute('draggable', 'false'); | ||||
|         item.setAttribute('aria-grabbed', 'false'); | ||||
|         item = null; | ||||
|       } | ||||
|       event.preventDefault(); | ||||
|     }); | ||||
|  | ||||
|     // Mouse interaction | ||||
|     // - mouse down, enable "draggable" and allow to drag the row, | ||||
|     // - mouse up, disable "draggable" | ||||
|     this.addEventListener('mousedown', ({ | ||||
|       target | ||||
|     }) => { | ||||
|       if (touched) return; | ||||
|  | ||||
|       // Check for .move button | ||||
|       const handler = getMoveHandler(target); | ||||
|       const row = handler ? handler.closest(that.repeatableElement) : null; | ||||
|       if (!row || row.closest('joomla-field-subform') !== that) { | ||||
|         return; | ||||
|       } | ||||
|       row.setAttribute('draggable', 'true'); | ||||
|       row.setAttribute('aria-grabbed', 'true'); | ||||
|       item = row; | ||||
|     }); | ||||
|     this.addEventListener('mouseup', () => { | ||||
|       if (item && !touched) { | ||||
|         item.setAttribute('draggable', 'false'); | ||||
|         item.setAttribute('aria-grabbed', 'false'); | ||||
|         item = null; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Keyboard interaction | ||||
|     // - "tab" to navigate to needed row, | ||||
|     // - modifier (ctr,alt,shift) + "space" select the row, | ||||
|     // - "tab" to select destination, | ||||
|     // - "enter" to place selected row in to destination | ||||
|     // - "esc" to cancel selection | ||||
|     this.addEventListener('keydown', event => { | ||||
|       if (event.code !== KEYCODE.ESC && event.code !== KEYCODE.SPACE && event.code !== KEYCODE.ENTER || event.target.form || !event.target.matches(that.repeatableElement)) { | ||||
|         return; | ||||
|       } | ||||
|       const row = event.target; | ||||
|  | ||||
|       // Make sure we handle correct children | ||||
|       if (!row || row.closest('joomla-field-subform') !== that) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Space is the selection or unselection keystroke | ||||
|       if (event.code === KEYCODE.SPACE && hasModifier(event)) { | ||||
|         // Unselect previously selected | ||||
|         if (row.getAttribute('aria-grabbed') === 'true') { | ||||
|           row.setAttribute('draggable', 'false'); | ||||
|           row.setAttribute('aria-grabbed', 'false'); | ||||
|           item = null; | ||||
|         } else { | ||||
|           // Select new | ||||
|           // If there was previously selected | ||||
|           if (item) { | ||||
|             item.setAttribute('draggable', 'false'); | ||||
|             item.setAttribute('aria-grabbed', 'false'); | ||||
|             item = null; | ||||
|           } | ||||
|  | ||||
|           // Mark new selection | ||||
|           row.setAttribute('draggable', 'true'); | ||||
|           row.setAttribute('aria-grabbed', 'true'); | ||||
|           item = row; | ||||
|         } | ||||
|  | ||||
|         // Prevent default to suppress any native actions | ||||
|         event.preventDefault(); | ||||
|       } | ||||
|  | ||||
|       // Escape is the cancel keystroke (for any target element) | ||||
|       if (event.code === KEYCODE.ESC && item) { | ||||
|         item.setAttribute('draggable', 'false'); | ||||
|         item.setAttribute('aria-grabbed', 'false'); | ||||
|         item = null; | ||||
|       } | ||||
|  | ||||
|       // Enter, to place selected item in selected position | ||||
|       if (event.code === KEYCODE.ENTER && item) { | ||||
|         item.setAttribute('draggable', 'false'); | ||||
|         item.setAttribute('aria-grabbed', 'false'); | ||||
|  | ||||
|         // Do nothing here | ||||
|         if (row === item) { | ||||
|           item = null; | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         // Move the item to selected position | ||||
|         switchRowPositions(item, row); | ||||
|         event.preventDefault(); | ||||
|         item = null; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // dragstart event to initiate mouse dragging | ||||
|     this.addEventListener('dragstart', ({ | ||||
|       dataTransfer | ||||
|     }) => { | ||||
|       if (item) { | ||||
|         // We going to move the row | ||||
|         dataTransfer.effectAllowed = 'move'; | ||||
|  | ||||
|         // This need to work in Firefox and IE10+ | ||||
|         dataTransfer.setData('text', ''); | ||||
|       } | ||||
|     }); | ||||
|     this.addEventListener('dragover', event => { | ||||
|       if (item) { | ||||
|         event.preventDefault(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Handle drag action, move element to hovered position | ||||
|     this.addEventListener('dragenter', ({ | ||||
|       target | ||||
|     }) => { | ||||
|       // Make sure the target in the correct container | ||||
|       if (!item || target.parentElement.closest('joomla-field-subform') !== that) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Find a hovered row | ||||
|       const row = target.closest(that.repeatableElement); | ||||
|  | ||||
|       // One more check for correct parent | ||||
|       if (!row || row.closest('joomla-field-subform') !== that) return; | ||||
|       switchRowPositions(item, row); | ||||
|     }); | ||||
|  | ||||
|     // dragend event to clean-up after drop or cancelation | ||||
|     // which fires whether or not the drop target was valid | ||||
|     this.addEventListener('dragend', () => { | ||||
|       if (item) { | ||||
|         item.setAttribute('draggable', 'false'); | ||||
|         item.setAttribute('aria-grabbed', 'false'); | ||||
|         item = null; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     /** | ||||
|      * Move UP, Move Down sorting | ||||
|      */ | ||||
|     const btnUp = `${that.buttonMove}-up`; | ||||
|     const btnDown = `${that.buttonMove}-down`; | ||||
|     this.addEventListener('click', ({ | ||||
|       target | ||||
|     }) => { | ||||
|       if (target.closest('joomla-field-subform') !== this) { | ||||
|         return; | ||||
|       } | ||||
|       const btnUpEl = target.closest(btnUp); | ||||
|       const btnDownEl = !btnUpEl ? target.closest(btnDown) : null; | ||||
|       if (!btnUpEl && !btnDownEl) { | ||||
|         return; | ||||
|       } | ||||
|       let row = (btnUpEl || btnDownEl).closest(that.repeatableElement); | ||||
|       row = row && row.closest('joomla-field-subform') === this ? row : null; | ||||
|       if (!row) { | ||||
|         return; | ||||
|       } | ||||
|       const rows = this.getRows(); | ||||
|       const curIdx = rows.indexOf(row); | ||||
|       let dstIdx = 0; | ||||
|       if (btnUpEl) { | ||||
|         dstIdx = curIdx - 1; | ||||
|         dstIdx = dstIdx < 0 ? rows.length - 1 : dstIdx; | ||||
|       } else { | ||||
|         dstIdx = curIdx + 1; | ||||
|         dstIdx = dstIdx > rows.length - 1 ? 0 : dstIdx; | ||||
|       } | ||||
|       switchRowPositions(row, rows[dstIdx]); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| customElements.define('joomla-field-subform', JoomlaFieldSubform); | ||||
		Reference in New Issue
	
	Block a user