joomlagerman / joomla

The J!German translation team provides German translation for Joomla!
https://www.jgerman.de
GNU General Public License v2.0
25 stars 46 forks source link

[5.1] Subform rows sorting with buttons, addittionaly to drag and drop #3083

Closed jgerman-bot closed 8 months ago

jgerman-bot commented 8 months ago

New language relevant PR in upstream repo: https://github.com/joomla/joomla-cms/pull/42334 Here are the upstream changes:

Click to expand the diff! ```diff diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index 4adc9b185f4e1..04a50421f9024 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -433,6 +433,8 @@ JGLOBAL_FIELD_MODIFIED_BY_DESC="The user who did the last modification." JGLOBAL_FIELD_MODIFIED_BY_LABEL="Modified By" JGLOBAL_FIELD_MODIFIED_LABEL="Modified Date" JGLOBAL_FIELD_MOVE="Move" +JGLOBAL_FIELD_MOVE_DOWN="Move down" +JGLOBAL_FIELD_MOVE_UP="Move up" JGLOBAL_FIELD_NUM_CATEGORY_ITEMS_DESC="Number of categories to display for each level." JGLOBAL_FIELD_NUM_CATEGORY_ITEMS_LABEL="Number of Categories" JGLOBAL_FIELD_PUBLISH_DOWN_DESC="An optional date to stop publishing." diff --git a/api/language/en-GB/joomla.ini b/api/language/en-GB/joomla.ini index fc0b6dfa82a1f..152a4a8bca4bd 100644 --- a/api/language/en-GB/joomla.ini +++ b/api/language/en-GB/joomla.ini @@ -427,6 +427,8 @@ JGLOBAL_FIELD_MODIFIED_BY_DESC="The user who did the last modification." JGLOBAL_FIELD_MODIFIED_BY_LABEL="Modified By" JGLOBAL_FIELD_MODIFIED_LABEL="Modified Date" JGLOBAL_FIELD_MOVE="Move" +JGLOBAL_FIELD_MOVE_DOWN="Move down" +JGLOBAL_FIELD_MOVE_UP="Move up" JGLOBAL_FIELD_NUM_CATEGORY_ITEMS_DESC="Number of categories to display for each level." JGLOBAL_FIELD_NUM_CATEGORY_ITEMS_LABEL="Number of Categories" JGLOBAL_FIELD_PUBLISH_DOWN_DESC="An optional date to stop publishing." diff --git a/build/media_source/system/js/fields/joomla-field-subform.w-c.es6.js b/build/media_source/system/js/fields/joomla-field-subform.w-c.es6.js index 12de2790a5fd0..eaafaf052aea8 100644 --- a/build/media_source/system/js/fields/joomla-field-subform.w-c.es6.js +++ b/build/media_source/system/js/fields/joomla-field-subform.w-c.es6.js @@ -3,587 +3,611 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -((customElements) => { - 'use strict'; +const KEYCODE = { + SPACE: 'Space', + ESC: 'Escape', + ENTER: 'Enter', +}; - const KEYCODE = { - SPACE: 32, - ESC: 27, - ENTER: 13, - }; +/** + * Helper for testing whether a selection modifier is pressed + * @param {Event} event + * + * @returns {boolean|*} + */ +function hasModifier(event) { + return (event.ctrlKey || event.metaKey || event.shiftKey); +} - /** - * 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'); } - class JoomlaFieldSubform extends HTMLElement { - // Attribute getters - get buttonAdd() { return this.getAttribute('button-add'); } + get buttonRemove() { return this.getAttribute('button-remove'); } - get buttonRemove() { return this.getAttribute('button-remove'); } + get buttonMove() { return this.getAttribute('button-move'); } - get buttonMove() { return this.getAttribute('button-move'); } + get rowsContainer() { return this.getAttribute('rows-container'); } - get rowsContainer() { return this.getAttribute('rows-container'); } + get repeatableElement() { return this.getAttribute('repeatable-element'); } - get repeatableElement() { return this.getAttribute('repeatable-element'); } + get minimum() { return this.getAttribute('minimum'); } - get minimum() { return this.getAttribute('minimum'); } + get maximum() { return this.getAttribute('maximum'); } - get maximum() { return this.getAttribute('maximum'); } + get name() { return this.getAttribute('name'); } - 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}`); - 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); + } - this.setAttribute('name', value); - } + constructor() { + super(); - constructor() { - super(); + const that = this; - const that = this; + // Get the rows container + this.containerWithRows = this; - // Get the rows container - this.containerWithRows = this; + if (this.rowsContainer) { + const allContainers = this.querySelectorAll(this.rowsContainer); - 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; + } + }); + } - // 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; - // 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 = ''; - // Template for the repeating group - this.template = ''; + // Prepare a row template, and find available field names + this.prepareTemplate(); - // 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; - // 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.buttonAdd) { - btnAdd = event.target.matches(that.buttonAdd) - ? event.target - : event.target.closest(that.buttonAdd); - } + if (that.buttonRemove) { + btnRem = event.target.closest(that.buttonRemove); + } - if (that.buttonRemove) { - btnRem = event.target.matches(that.buttonRemove) - ? event.target - : 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(); + } + }); - // 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.keyCode !== 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(); - } - }); - } + 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); - // Sorting - if (this.buttonMove) { - this.setUpDragSort(); - } - } + 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; - /** - * 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); + if (isRem && row) { + that.removeRow(row); + } else if (isAdd) { + that.addRow(row); + } + event.preventDefault(); } }); - - 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'); - } + // Sorting + if (this.buttonMove) { + this.setUpDragSort(); } + } - /** - * 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; + /** + * 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); } + }); - // 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]; + return result; + } - // Add to container - if (after) { - after.parentNode.insertBefore(row, after.nextSibling); - } else { - this.containerWithRows.append(row); - } + /** + * Prepare a row template + */ + prepareTemplate() { + const tmplElement = [].slice.call(this.children).filter((el) => el.classList.contains('subform-repeatable-template-section')); - // Add draggable attributes - if (this.buttonMove) { - row.setAttribute('draggable', 'false'); - row.setAttribute('aria-grabbed', 'false'); - row.setAttribute('tabindex', '0'); - } + if (tmplElement[0]) { + this.template = tmplElement[0].innerHTML; + } - // Marker that it is new - row.setAttribute('data-new', '1'); - // Fix names and ids, and reset values - this.fixUniqueAttributes(row, count); + if (!this.template) { + throw new Error('The row template is required for the subform element to work'); + } + } - // Tell about the new row - this.dispatchEvent(new CustomEvent('subform-row-add', { - detail: { row }, - bubbles: true, - })); + /** + * 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; + } - row.dispatchEvent(new CustomEvent('joomla:updated', { - bubbles: true, - cancelable: true, - })); + // 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); + } - return row; + // Add draggable attributes + if (this.buttonMove) { + row.setAttribute('draggable', 'false'); + row.setAttribute('aria-grabbed', 'false'); + row.setAttribute('tabindex', '0'); } - /** - * Remove the row - * @param {HTMLElement} row - */ - removeRow(row) { - // Count how much we have - const count = this.getRows().length; - if (count <= this.minimum) { - return; - } + // Marker that it is new + row.setAttribute('data-new', '1'); + // Fix names and ids, and reset values + this.fixUniqueAttributes(row, count); - // Tell about the row will be removed - this.dispatchEvent(new CustomEvent('subform-row-remove', { - detail: { row }, - bubbles: true, - })); + // Tell about the new row + this.dispatchEvent(new CustomEvent('subform-row-add', { + detail: { row }, + bubbles: true, + })); - row.dispatchEvent(new CustomEvent('joomla:removed', { - bubbles: true, - cancelable: true, - })); + row.dispatchEvent(new CustomEvent('joomla:updated', { + bubbles: true, + cancelable: true, + })); - row.parentNode.removeChild(row); + 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; } - /** - * 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; - } + // Tell about the row will be removed + this.dispatchEvent(new CustomEvent('subform-row-remove', { + detail: { row }, + bubbles: true, + })); - return el.closest('joomla-field-subform') === this; - }); + row.dispatchEvent(new CustomEvent('joomla:removed', { + bubbles: true, + cancelable: true, + })); - 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 - let forOldAttr = id; // Fix "for" in the labels - - if ($el.type === 'checkbox' && name.match(/\[\]$/)) { // fix - // Recount id - countMulti = ids[id] ? ids[id].length : 0; - if (!countMulti) { - // Set the id for fieldset and group label - const fieldset = $el.closest('fieldset.checkboxes'); - - const elLbl = row.querySelector(`label[for="${id}"]`); - - if (fieldset) { - fieldset.setAttribute('id', idNew); - } - - if (elLbl) { - elLbl.setAttribute('for', idNew); - elLbl.setAttribute('id', `${idNew}-lbl`); - } - } - forOldAttr += countMulti; - idNew += countMulti; - } else if ($el.type === 'radio') { // fix - // Recount id - countMulti = ids[id] ? ids[id].length : 0; - if (!countMulti) { - // Set the id for fieldset and group label - const fieldset = $el.closest('fieldset.radio'); - - const elLbl = row.querySelector(`label[for="${id}"]`); - - if (fieldset) { - fieldset.setAttribute('id', idNew); - } - - if (elLbl) { - elLbl.setAttribute('for', idNew); - elLbl.setAttribute('id', `${idNew}-lbl`); - } + 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 + let forOldAttr = id; // Fix "for" in the labels + + if ($el.type === 'checkbox' && name.match(/\[\]$/)) { // fix + // Recount id + countMulti = ids[id] ? ids[id].length : 0; + if (!countMulti) { + // Set the id for fieldset and group label + const fieldset = $el.closest('fieldset.checkboxes'); + + const elLbl = row.querySelector(`label[for="${id}"]`); + + if (fieldset) { + fieldset.setAttribute('id', idNew); } - forOldAttr += countMulti; - idNew += countMulti; - } - // Cache already used id - if (ids[id]) { - ids[id].push(true); - } else { - ids[id] = [true]; + if (elLbl) { + elLbl.setAttribute('for', idNew); + elLbl.setAttribute('id', `${idNew}-lbl`); + } } + forOldAttr += countMulti; + idNew += countMulti; + } else if ($el.type === 'radio') { // fix + // Recount id + countMulti = ids[id] ? ids[id].length : 0; + if (!countMulti) { + // Set the id for fieldset and group label + const fieldset = $el.closest('fieldset.radio'); + + const elLbl = row.querySelector(`label[for="${id}"]`); + + if (fieldset) { + fieldset.setAttribute('id', idNew); + } - // Replace the name to new one - $el.name = nameNew; - if ($el.id) { - $el.id = idNew; + if (elLbl) { + elLbl.setAttribute('for', idNew); + elLbl.setAttribute('id', `${idNew}-lbl`); + } } + forOldAttr += countMulti; + idNew += countMulti; + } - if (aria) { - $el.setAttribute('aria-describedby', `${nameNew}-desc`); - } + // Cache already used id + if (ids[id]) { + ids[id].push(true); + } else { + ids[id] = [true]; + } - // Check if there is a label for this input - const lbl = row.querySelector(`label[for="${forOldAttr}"]`); - if (lbl) { - lbl.setAttribute('for', idNew); - lbl.setAttribute('id', `${idNew}-lbl`); - } - }); - } + // Replace the name to new one + $el.name = nameNew; + if ($el.id) { + $el.id = idNew; + } - /** - * 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 - const rows = Array.from(this.getRows()); - - rows.forEach((row) => { - row.setAttribute('draggable', 'false'); - row.setAttribute('aria-grabbed', 'false'); - row.setAttribute('tabindex', '0'); - }); + if (aria) { + $el.setAttribute('aria-describedby', `${nameNew}-desc`); + } - // 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); + // Check if there is a label for this input + const lbl = row.querySelector(`label[for="${forOldAttr}"]`); + if (lbl) { + lbl.setAttribute('for', idNew); + lbl.setAttribute('id', `${idNew}-lbl`); } + }); + } - // 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; - } - } - } + /** + * 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); + } - if (isRowBefore) { - dest.parentNode.insertBefore(src, dest); - } else { - dest.parentNode.insertBefore(src, dest.nextSibling); + // 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; + } } } - /** - * 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; + if (isRowBefore) { + dest.parentNode.insertBefore(src, dest); + } else { + dest.parentNode.insertBefore(src, dest.nextSibling); + } + } - // Check for .move button - const handler = getMoveHandler(event.target); + /** + * 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; - const row = handler ? handler.closest(that.repeatableElement) : null; + // Check for .move button + const handler = getMoveHandler(event.target); - if (!row || row.closest('joomla-field-subform') !== that) { - return; - } + const row = handler ? handler.closest(that.repeatableElement) : null; - // 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); - } + if (!row || row.closest('joomla-field-subform') !== that) { + return; + } - item.setAttribute('draggable', 'false'); - item.setAttribute('aria-grabbed', 'false'); - item = null; + // 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); } - event.preventDefault(); - }); + item.setAttribute('draggable', 'false'); + item.setAttribute('aria-grabbed', 'false'); + item = null; + } - // Mouse interaction - // - mouse down, enable "draggable" and allow to drag the row, - // - mouse up, disable "draggable" - this.addEventListener('mousedown', ({ target }) => { - if (touched) return; + event.preventDefault(); + }); - // Check for .move button - const handler = getMoveHandler(target); + // Mouse interaction + // - mouse down, enable "draggable" and allow to drag the row, + // - mouse up, disable "draggable" + this.addEventListener('mousedown', ({ target }) => { + if (touched) return; - const row = handler ? handler.closest(that.repeatableElement) : null; + // Check for .move button + const handler = getMoveHandler(target); - if (!row || row.closest('joomla-field-subform') !== that) { - return; - } + const row = handler ? handler.closest(that.repeatableElement) : null; - row.setAttribute('draggable', 'true'); - row.setAttribute('aria-grabbed', 'true'); - item = row; - }); + if (!row || row.closest('joomla-field-subform') !== that) { + return; + } - this.addEventListener('mouseup', () => { - if (item && !touched) { - item.setAttribute('draggable', 'false'); - item.setAttribute('aria-grabbed', 'false'); - item = null; - } - }); + row.setAttribute('draggable', 'true'); + row.setAttribute('aria-grabbed', 'true'); + item = row; + }); - // 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.keyCode !== KEYCODE.ESC - && event.keyCode !== KEYCODE.SPACE - && event.keyCode !== KEYCODE.ENTER) || event.target.form - || !event.target.matches(that.repeatableElement)) { - return; - } + 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; + const row = event.target; - // Make sure we handle correct children - if (!row || row.closest('joomla-field-subform') !== that) { - return; - } + // Make sure we handle correct children + if (!row || row.closest('joomla-field-subform') !== that) { + return; + } - // Space is the selection or unselection keystroke - if (event.keyCode === KEYCODE.SPACE && hasModifier(event)) { - // Unselect previously selected - if (row.getAttribute('aria-grabbed') === 'true') { - row.setAttribute('draggable', 'false'); - row.setAttribute('aria-grabbed', 'false'); + // 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; - } 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(); + // Mark new selection + row.setAttribute('draggable', 'true'); + row.setAttribute('aria-grabbed', 'true'); + item = row; } - // Escape is the cancel keystroke (for any target element) - if (event.keyCode === KEYCODE.ESC && item) { - item.setAttribute('draggable', 'false'); - item.setAttribute('aria-grabbed', 'false'); + // 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; } - // Enter, to place selected item in selected position - if (event.keyCode === KEYCODE.ENTER && item) { - item.setAttribute('draggable', 'false'); - item.setAttribute('aria-grabbed', 'false'); + // Move the item to selected position + switchRowPositions(item, row); - // Do nothing here - if (row === item) { - item = null; - return; - } + event.preventDefault(); + item = null; + } + }); - // Move the item to selected position - switchRowPositions(item, row); + // dragstart event to initiate mouse dragging + this.addEventListener('dragstart', ({ dataTransfer }) => { + if (item) { + // We going to move the row + dataTransfer.effectAllowed = 'move'; - event.preventDefault(); - item = null; - } - }); + // This need to work in Firefox and IE10+ + dataTransfer.setData('text', ''); + } + }); - // dragstart event to initiate mouse dragging - this.addEventListener('dragstart', ({ dataTransfer }) => { - if (item) { - // We going to move the row - dataTransfer.effectAllowed = 'move'; + this.addEventListener('dragover', (event) => { + if (item) { + event.preventDefault(); + } + }); - // This need to work in Firefox and IE10+ - dataTransfer.setData('text', ''); - } - }); + // 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; + } - this.addEventListener('dragover', (event) => { - if (item) { - event.preventDefault(); - } - }); + // Find a hovered row + const row = target.closest(that.repeatableElement); - // 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; - } + // One more check for correct parent + if (!row || row.closest('joomla-field-subform') !== that) return; - // Find a hovered row - const row = target.closest(that.repeatableElement); + switchRowPositions(item, row); + }); - // One more check for correct parent - if (!row || row.closest('joomla-field-subform') !== that) return; + // 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; + } + }); - switchRowPositions(item, row); - }); + /** + * 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; - // 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; - } - }); - } + 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); -})(customElements); +customElements.define('joomla-field-subform', JoomlaFieldSubform); diff --git a/build/media_source/templates/administrator/atum/scss/blocks/_form.scss b/build/media_source/templates/administrator/atum/scss/blocks/_form.scss index 562d7757bdcc2..b86634cf04f2a 100644 --- a/build/media_source/templates/administrator/atum/scss/blocks/_form.scss +++ b/build/media_source/templates/administrator/atum/scss/blocks/_form.scss @@ -194,13 +194,27 @@ div.subform-repeatable-group { top: 50%; right: 100%; padding: 0; - margin-top: -27px; border-radius: $border-radius 0 0 $border-radius; + transform: translateY(-50%); span { padding: 1.5rem .5rem; } } + &.group-move-up { + top: 50%; + right: 100%; + margin-top: -45px; + border-radius: 0; + transform: translateY(-50%); + } + &.group-move-down { + top: 50%; + right: 100%; + margin-top: 45px; + border-radius: 0; + transform: translateY(-50%); + } } } } diff --git a/build/media_source/templates/site/cassiopeia/scss/blocks/_form.scss b/build/media_source/templates/site/cassiopeia/scss/blocks/_form.scss index c204209c31423..438cb3f7a2c16 100644 --- a/build/media_source/templates/site/cassiopeia/scss/blocks/_form.scss +++ b/build/media_source/templates/site/cassiopeia/scss/blocks/_form.scss @@ -148,3 +148,76 @@ fieldset { } } } + +// Subform - non table layout +div.subform-repeatable-group { + position: relative; + padding: 32px 32px 16px 28px; + margin-top: 20px; + margin-left: 32px; + border: $input-border-width solid $input-border-color; + @include border-radius($border-radius); + + > .control-group { + margin-top: 0; + } + + > .btn-toolbar { + + .btn-group { + position: static; + margin: 0; + } + + .btn { + position: absolute; + + &.group-add { + right: -1px; + bottom: -1px; + border-radius: $border-radius 0 $border-radius 0; + } + &.group-remove { + top: -1px; + right: -1px; + border-radius: 0 $border-radius 0 $border-radius; + } + &.group-move { + top: 50%; + right: 100%; + padding: 0; + border-radius: $border-radius 0 0 $border-radius; + transform: translateY(-50%); + + span { + padding: 1.5rem .5rem; + } + } + &.group-move-up { + top: 50%; + right: 100%; + margin-top: -45px; + border-radius: 0; + transform: translateY(-50%); + } + &.group-move-down { + top: 50%; + right: 100%; + margin-top: 45px; + border-radius: 0; + transform: translateY(-50%); + } + } + } +} + +// Highlight draggable section +.subform-repeatable-group[draggable="true"] { + // For non table layout + background-color: $teal; + + // For table layout + > td { + background-color: $teal; + } +} diff --git a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php index 4710dc0b04540..4713233ba9b60 100644 --- a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php +++ b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php @@ -35,9 +35,11 @@ - + + + + +
diff --git a/layouts/joomla/form/field/subform/repeatable/section.php b/layouts/joomla/form/field/subform/repeatable/section.php index 7eb4826223d2c..72552d4dedbd5 100644 --- a/layouts/joomla/form/field/subform/repeatable/section.php +++ b/layouts/joomla/form/field/subform/repeatable/section.php @@ -35,9 +35,11 @@ - + + + + + ```