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 @@
-
+
+
+
+
+