diff --git a/corsscripts/stacksortable.js b/corsscripts/stacksortable.js
index 2bb75f8632d71d36b77ef99b4e7a7137f06ce5a5..88a0d2fa2fdd08319ee3d1f351dc1009596ece4f 100644
--- a/corsscripts/stacksortable.js
+++ b/corsscripts/stacksortable.js
@@ -71,8 +71,8 @@ export function preprocess_steps(steps, sortableUserOpts, headers, available_hea
// Validate the object
var valid = _validate_parsons_JSON(steps);
- // At this point, we know steps is either a flat JSON, or it's top-level keys are a subset of
- // ["steps", "options", "headers", "available_header", "index"], and contains at least "steps".
+ // At this point, we know steps is either a flat JSON, or it's top-level keys are a subset of
+ // ["steps", "options", "headers", "available_header", "index"], and contains at least "steps".
// Separate these if they are present.
if (_validate_top_level_keys_JSON(steps, ["steps", "options", "headers", "index", "available_header"], ["steps"])) {
var sortableUserOpts = steps["options"];
@@ -116,8 +116,8 @@ function _stackstring_objectify(stackjson_array_string) {
* The function checks the structure of the provided Parson's JSON (`steps`)
* to ensure it follows specific patterns:
* 1. If the JSON has depth 1, it should be a valid flat JSON (i.e., should have string values).
- * 2. If the JSON has depth 2, the top-level keys should be a subset of
- * `["steps", "options", "headers", "index", "available_header"]`, and must contain `"steps"`.
+ * 2. If the JSON has depth 2, the top-level keys should be a subset of
+ * `["steps", "options", "headers", "index", "available_header"]`, and must contain `"steps"`.
* The value for "steps" should be a valid flat JSON. Options are not validated here.
*
* @param {Object} steps - The Parson's JSON to be validated.
@@ -154,7 +154,7 @@ function _stackstring_objectify(stackjson_array_string) {
*/
function _validate_parsons_JSON(steps) {
// If the JSON has depth 1 then it should be a valid flat JSON (i.e., should have string values).
- if (Object.values(steps).every((val) => !(typeof(val) == 'object'))) {
+ if (Object.values(steps).every((val) => !(typeof(val) == "object"))) {
return _validate_flat_steps(steps);
}
// Else the top-level of the JSON should have keys that are a subset of ["steps", "options", "headers", "index", "available_header"]
@@ -205,16 +205,16 @@ function _validate_parsons_JSON(steps) {
*/
function _validate_flat_steps(steps) {
// Case when steps are coming from a Maxima variable: convert to a JSON
- if (typeof(steps) == 'string') {
+ if (typeof(steps) == "string") {
steps = _stackstring_objectify(steps);
}
- return Object.values(steps).every((val) => typeof(val) == 'string');
+ return Object.values(steps).every((val) => typeof(val) == "string");
}
/**
- * Validates the top-level keys of a JSON object by checking they are a subset of `validKeys`
+ * Validates the top-level keys of a JSON object by checking they are a subset of `validKeys`
* and a superset of `requiredKeys`.
- *
+ *
* @param {Object} JSON - The JSON object to validate.
* @param {Array} validKeys - An array of valid top-level keys.
* @param {Array} requiredKeys - An array of top-level keys that are required.
@@ -222,8 +222,8 @@ function _validate_flat_steps(steps) {
*/
function _validate_top_level_keys_JSON(JSON, validKeys, requiredKeys) {
const keys = Object.keys(JSON);
- const missingRequiredKeys = requiredKeys.filter(key => !keys.includes(key));
- const invalidKeys = keys.filter(key => !validKeys.includes(key));
+ const missingRequiredKeys = requiredKeys.filter((key) => !keys.includes(key));
+ const invalidKeys = keys.filter((key) => !validKeys.includes(key));
return invalidKeys.length === 0 && missingRequiredKeys.length === 0;
}
@@ -240,12 +240,18 @@ export function get_iframe_height() {
* Class for for managing Sortable lists for Parson's block questions in STACK.
*
* @class
- * @param {Object} steps - Object containing flat steps JSON.
- * @param {string} availableId - ID of the available list element.
- * @param {string} usedId - ID of the used list element.
- * @param {string|null} inputId - ID of the input element for storing state (optional).
- * @param {Object|null} options - Custom options for sortable lists (optional).
- * @param {boolean} clone - Flag indicating whether to clone elements during drag-and-drop.
+ * @param {Object} steps - The flat steps object.
+ * @param {string|null} inputId - The ID of the input element storing the state data.
+ * @param {Object|null} options - Options for the Sortable lists.
+ * @param {boolean} clone - Indicates whether clone mode is being used in the `parsons` block.
+ * @param {number} columns - The number of columns being used, default is 1.
+ * @param {number|null} rows - The number of rows.
+ * @param {string} orientation - The orientation of the Parsons object. Default is "col".
+ * @param {string} index - The index for the proof steps. Default is an empty string.
+ * @param {boolean} grid - False if the `parsons` block is being used for proof, true if for grouping and matching.
+ * Affects styling.
+ * @param {string|null} item_height - The height of each item in the sortable lists (including headers and indexes).
+ * @param {string|null} item_width - The width of each item in the sortable lists (including headers and indexes).
*
* @property {Object} steps - Object containing all steps.
* @property {string} inputId - ID of the input element for storing state (optional).
@@ -253,77 +259,109 @@ export function get_iframe_height() {
* @property {Object} userOptions - User-defined options merged with default options.
* @property {boolean} clone - Flag indicating whether to clone elements during sorting.
* @property {Object} options - Final options for sortable lists.
+ * @property {String} columns - Number of columns being used.
+ * @property {String} rows - Number of rows being used.
+ * @property {Array} index - List of index items.
+ * @property {boolean} use_index - Whether an index has been passed to the constructor or not.
+ * @property {boolean} grid - Whether grid styling is to be applied (i.e., false if using for proof).
+ * @property {string} item_class - The style class to use, different for proof vs. matching.
+ * @property {Object} item_height_width - If item_height and item_width are passed to the constructor, object containing them.
+ * @property {Object} container_height_width - Add padding to this.item_height_width to allow custom heights/widths to work.
+ * @property {Object} ids - Contains DOM ids for used and available lists.
+ * @property {Array} usedId - Two-dimensional array containing used list DOM ids.
+ * @property {string} availableId - String containing the available list DOM id.
+ * @property {Object} defaultOptions - Default sortable options.
*
+ * @method add_dblclick_listeners - Add listeners that moves items on double-click and updates state for proofs only.
+ * @method add_delete_all_listener - Adds a listener that deletes all from the used list and updates state.
+ * @method add_headers - Adds header elements to the used and available lists.
+ * @method add_index - Add an index column if passed to the constructor.
+ * @method add_reorientation_button - Adds a button that allows user to flip between orientations on question page.
+ * @method create_row_col_divs - Generates the HTML row and column divs according to how columns and rows are passed to constructor.
* @method generate_available - Generates the available list based on the current state.
* @method generate_used - Generates the used list based on the current state.
- * @method add_headers - Adds header elements to the used and available lists.
* @method update_state - Updates the state based on changes in the used and available lists.
- * @method add_dblclick_listeners - Add listeners that moves items on double-click and updates state.
- * @method add_delete_all_listener - Adds a listener that deletes all from the used list and updates state.
+ * @method validate_options - Validates the sortable user options.
*
* @example
- * // Creating a StackSortable instance:
+ * // Creating a basic StackSortable instance for proof:
* const sortable = new stack_sortable({
* "step1": "Step 1",
* "step2": "Step 2",
* // ...
- * }, "availableList", "usedList", "stateInput", { used: { animation: 100 }, available: { animation: 100 } }, false);
+ * }, "ans1", { used: { animation: 100 }, available: { animation: 100 } }, false);
*
* // Generating lists and adding headers:
* sortable.generate_available();
* sortable.generate_used();
- * sortable.add_headers({ used: { header: "Used Header" }, available: { header: "Available Header" } });
+ * sortable.add_headers();
+ *
+ * // Create the Sortable answer lists.
+ * var sortableUsed =
+ * stackSortable.ids.used.map((idList) =>
+ * idList.map((usedId) => Sortable.create(document.getElementById(usedId), stackSortable.options.used)));
*
- * // Updating state on changes:
- * sortable.update_state(newUsedList, newAvailableList);
+ * // Create the Sortable available list.
+ * var sortableAvailable = Sortable.create(availableList, stackSortable.options.available);
*
- * // Updating state on double-click events:
- * sortable.update_state_dblclick(newUsedList, newAvailableList);
+ * // Add the state callback function for all the created sortables.
+ * sortableUsed.forEach((sortableList) =>
+ * sortableList.forEach((sortable) =>
+ * sortable.option("onSort", () => {
+ * stackSortable.update_state(sortableUsed, sortableAvailable);})
+ * )
+ * );
+ * sortableAvailable.option("onSort", () => {stackSortable.update_state(sortableUsed, sortableAvailable);});
*
* @exports stack_sortable
*/
-export const stack_sortable = class {
+export const stack_sortable = class stack_sortable {
/**
* Constructor for the StackSortable class.
*
* @constructor
- * @param {Object} steps - Object containing the flat steps JSON.
- * @param {string} availableId - ID of the available list element.
- * @param {string} usedId - ID of the used list element.
- * @param {string|null} inputId - ID of the input element for storing state (optional).
- * @param {Object|null} options - Custom options for sortable lists
- * of form {used: UsedOptions, available: AvailableOptions} (optional).
- * @param {boolean} clone - Flag indicating whether to clone elements during sorting.
+ * @param {Object} steps - The flat steps object.
+ * @param {string|null} inputId - The ID of the input element storing the state data.
+ * @param {Object|null} options - Options for the Sortable lists.
+ * @param {boolean} clone - Indicates whether clone mode is being used in the `parsons` block.
+ * @param {number} columns - The number of columns being used, default is 1.
+ * @param {number|null} rows - The number of rows.
+ * @param {string} orientation - The orientation of the Parsons object. Default is "col".
+ * @param {string} index - The index for the proof steps. Default is an empty string.
+ * @param {boolean} grid - False if the `parsons` block is being used for proof, true if for grouping and matching.
+ * Affects styling.
+ * @param {string|null} item_height - The height of each item in the sortable lists (including headers and indexes).
+ * @param {string|null} item_width - The width of each item in the sortable lists (including headers and indexes).
*/
- constructor(steps,
- inputId = null,
- options = null,
- clone = false,
- columns = 1,
- rows = null,
- orientation = "col",
- index = "",
- grid = false,
- item_height = null,
+ constructor(steps,
+ inputId = null,
+ options = null,
+ clone = false,
+ columns = 1,
+ rows = null,
+ orientation = "col",
+ index = "",
+ grid = false,
+ item_height = null,
item_width = null) {
this.steps = steps;
this.inputId = inputId;
this.orientation = orientation;
- this.columns = (this.orientation === "col") ? columns : rows;
- this.rows = (this.orientation === "col") ? rows : columns;
+ this.columns = ((this.orientation === "col") ? columns : rows);
+ this.rows = ((this.orientation === "col") ? rows : columns);
this.index = index;
this.use_index = this.index !== "";
this.grid = grid;
- this.item_class = this.grid ?
- (this.orientation === "row" ? "grid-item-rigid" : "grid-item") : "list-group-item";
- this.item_height_width = {'style' : ''};
- for (const [key, val] of [['height', item_height], ['width', item_width]]) {
- if (val !== '') {this.item_height_width['style'] += `${key}:${val}px;`};
+ this.item_class = (
+ this.grid ? (this.orientation === "row" ? "grid-item-rigid" : "grid-item") : "list-group-item"
+ );
+ this.item_height_width = {"style" : ""};
+ for (const [key, val] of [["height", item_height], ["width", item_width]]) {
+ if (val !== "") {this.item_height_width["style"] += `${key}:${val}px;`};
};
- this.item_height_width = (this.item_height_width['style'] === '') ? {} : this.item_height_width;
- this.item_height = (item_height !== '') ? {'style' : `height:${item_height}px;`} : {};
- this.item_width = (item_width !== '') ? {'style' : `width:${item_width}px;`} : {};
- this.container_height_width = (this.item_height_width['style'] !== '') ? {'style' : this.item_height_width['style'] + 'margin: 12px;'} : {};
+ this.item_height_width = (this.item_height_width["style"] === "") ? {} : this.item_height_width;
+ this.container_height_width = (this.item_height_width["style"] !== "") ?
+ {"style" : this.item_height_width["style"] + "margin: 12px;"} : {};
this.state = this._generate_state(this.steps, inputId, Number(this.columns), Number(this.rows));
if (inputId !== null) {
this.input = document.getElementById(this.inputId);
@@ -333,13 +371,13 @@ export const stack_sortable = class {
this.availableId = this.ids.available;
this.usedId = this.ids.used;
this.clone = clone;
-
+
this.defaultOptions = {used: {animation: 50, cancel: ".header"}, available: {animation: 50, cancel: ".header"}};
// Merges user options and default, overwriting default with user options if they clash
this.userOptions = this._set_user_options(options);
// Create overall options from this.userOptions by setting ghostClass and group to required options
- // and overwriting them if they appear in userOptions. This also disables the list if they have been
+ // and overwriting them if they appear in userOptions. This also disables the list if they have been
// submitted.
this.options = this._set_ghostClass_group_and_disabled_options();
}
@@ -354,7 +392,7 @@ export const stack_sortable = class {
* @returns {void}
*/
add_dblclick_listeners(newUsed, newAvailable) {
- this.available.addEventListener('dblclick', (e) => {
+ this.available.addEventListener("dblclick", (e) => {
if (this._double_clickable(e.target)) {
// get highest-level parent
var li = this._get_moveable_parent_li(e.target);
@@ -363,7 +401,7 @@ export const stack_sortable = class {
this.update_state(newUsed, newAvailable);
}
});
- this.used[0][0].addEventListener('dblclick', (e) => {
+ this.used[0][0].addEventListener("dblclick", (e) => {
if (this._double_clickable(e.target)) {
// get highest-level parent
var li = this._get_moveable_parent_li(e.target);
@@ -388,7 +426,7 @@ export const stack_sortable = class {
*/
add_delete_all_listener(buttonId, newUsed, newAvailable) {
const button = document.getElementById(buttonId);
- button.addEventListener('click', () => {
+ button.addEventListener("click", () => {
this._delete_all_from_used(); this.update_state(newUsed, newAvailable);});
}
@@ -406,12 +444,13 @@ export const stack_sortable = class {
parentEl.insertBefore(header, parentEl.firstChild);
}
var parentEl = document.getElementById("availableList");
- parentEl.insertBefore(this._create_header(available_header, "availableHeader", this.item_height_width), parentEl.firstChild);
+ parentEl.insertBefore(
+ this._create_header(available_header, "availableHeader", this.item_height_width), parentEl.firstChild);
}
/**
* Adds index elements to the DOM based on the provided index array.
- *
+ *
* @param {Array} index - The array containing index values to be added.
*/
add_index(index) {
@@ -430,8 +469,8 @@ export const stack_sortable = class {
/**
* Adds a reorientation button to the document body.
- *
- * The button allows users to change the orientation of sortable lists between vertical
+ *
+ * The button allows users to change the orientation of sortable lists between vertical
* and horizontal.
*/
add_reorientation_button() {
@@ -446,9 +485,9 @@ export const stack_sortable = class {
}
/**
- * Populates the DOM with row and column div elements to the document based
+ * Populates the DOM with row and column div elements to the document based
* on how many columns and rows are being passed to the instance.
- *
+ *
* How this occurs depends on various configurations.
* - Lists should contain the `"row"` or `"col"` class according to the orientation.
* - If the class is being used for proof (i.e., `this.grid === false`), then the list class should
@@ -456,13 +495,13 @@ export const stack_sortable = class {
* - Items class depends only on the orientation.
*/
create_row_col_divs() {
- var usedClassList = (!this.grid || this.orientation === "col") ?
- ["list-group", this.orientation, "usedList"]:
- [this.orientation, "usedList"];
+ var usedClassList = (!this.grid || this.orientation === "col") ?
+ (["list-group", this.orientation, "usedList"]) :
+ ([this.orientation, "usedList"]);
var itemClass = (this.orientation === "col") ? "row" : "col";
var itemClassList = [itemClass, "usedList"];
var availClassList = (!this.grid || this.orientation === "col") ?
- ["list-group", this.orientation] :
+ ["list-group", this.orientation] :
[this.orientation];
var container = document.getElementById("containerRow");
@@ -472,7 +511,7 @@ export const stack_sortable = class {
indexCol.classList.add(...usedClassList);
container.append(indexCol);
}
- this.colIds.forEach((id) =>
+ this.colIds.forEach((id) =>
{
var colDiv = document.createElement("ul");
colDiv.id = id;
@@ -547,7 +586,7 @@ export const stack_sortable = class {
var newState = {used: newUsed.map((usedList) => usedList.map((used) => used.toArray())), available: newAvailable.toArray()};
if (this.inputId !== null) {
this.input.value = JSON.stringify(newState);
- this.input.dispatchEvent(new Event('change'));
+ this.input.dispatchEvent(new Event("change"));
}
this.state = newState;
}
@@ -569,7 +608,7 @@ export const stack_sortable = class {
* will appear on the question page.
*/
validate_options(possibleOptionKeys, unknownErr, overwrittenErr) {
- var err = '';
+ var err = "";
var keysRecognised = true;
var invalidKeys = [];
// If option is not recognised warn user
@@ -613,7 +652,7 @@ export const stack_sortable = class {
/**
* Applies attributes to an HTML element.
- *
+ *
* @param {HTMLElement} el - The HTML element to which attributes will be applied.
* @param {Object} opts - An object containing attribute-value pairs to be applied.
*/
@@ -625,7 +664,7 @@ export const stack_sortable = class {
/**
* Creates a header element with specified inner HTML, ID, and other attributes.
- *
+ *
* @param {string} innerHTML - The inner HTML content of the header element.
* @param {string} id - The ID attribute of the header element.
* @param {Object} attrs - An object containing additional attributes for the header element.
@@ -634,22 +673,22 @@ export const stack_sortable = class {
_create_header(innerHTML, id, attrs) {
let i = document.createElement("i");
i.innerHTML = innerHTML;
- var addClass = (this.orientation === "col") ?
- [this.item_class, 'header'] : [this.item_class, 'index'];
+ var addClass = (this.orientation === "col") ?
+ [this.item_class, "header"] : [this.item_class, "index"];
i.classList.add(...addClass);
this._apply_attrs(i, {...{"id" : id}, ...attrs});
return i;
}
/**
- * Creates and organizes identifiers for rows and columns. If only columns are passed, then
+ * Creates and organizes identifiers for rows and columns. If only columns are passed, then
* the used IDs will just be a flat list `["usedList_0", ..., "usedList_n"]`, where `columns = "n + 1"`.
* If both rows and columns have non-null values, then this will be a two-dimensional array
- * `[["usedList_00", "usedList_01", ..., "usedList_0n"], ["usedList_10", ...], ...]`.
- * In the two-dimensional case, a mapping between the column IDs `["usedList_0", ...]` and the
- * two-dimensional array of item IDs is contained in the object `this.rowColIds`, that is
+ * `[["usedList_00", "usedList_01", ..., "usedList_0n"], ["usedList_10", ...], ...]`.
+ * In the two-dimensional case, a mapping between the column IDs `["usedList_0", ...]` and the
+ * two-dimensional array of item IDs is contained in the object `this.rowColIds`, that is
* `this.rowColIds["usedList_0"] = ["usedList_00", "usedList_01", ...]`.
- *
+ *
* @param {number} rows - The number of rows.
* @param {number} columns - The number of columns.
* @returns {Object} - An object containing identifiers for used and available elements.
@@ -660,8 +699,8 @@ export const stack_sortable = class {
this.colIds = colIdx.map((idx) => `usedList_${idx}`);
this.rowColIds = {}
colIdx.forEach((i) => this.rowColIds[this.colIds[i]] = rowIdx.map((j) => `usedList_${j}${i}`));
- var usedIds = (rows === "") ?
- this.colIds.map((id) => [id]) :
+ var usedIds = (rows === "") ?
+ this.colIds.map((id) => [id]) :
Object.values(this.rowColIds);
return {
@@ -672,7 +711,7 @@ export const stack_sortable = class {
/**
* Creates an index element with specified inner HTML, ID, and additional attributes.
- *
+ *
* @param {string} innerHTML - The inner HTML content of the index element.
* @param {string} id - The ID attribute of the index element.
* @param {Object} attrs - An object containing additional attributes for the index element.
@@ -681,8 +720,8 @@ export const stack_sortable = class {
_create_index(innerHTML, id, attrs) {
let i = document.createElement("i");
i.innerHTML = innerHTML;
- var addClass = (this.orientation === "col") ?
- [this.item_class, 'index'] : [this.item_class, 'header'];
+ var addClass = (this.orientation === "col") ?
+ [this.item_class, "index"] : [this.item_class, "header"];
i.classList.add(...addClass);
this._apply_attrs(i, {...{"id" : id}, ...attrs});
return i;
@@ -690,7 +729,7 @@ export const stack_sortable = class {
/**
* Creates a list item (li) element containing the value of the specified key from `this.steps` and attributes.
- *
+ *
* @param {string} stepKey - The key whose HTML to get from `this.steps`.
* @param {Object} attrs - An object containing additional attributes for the list item element.
* @returns {HTMLElement} - The created list item (li) element.
@@ -705,7 +744,7 @@ export const stack_sortable = class {
/**
* Checks if a list item (li) is deletable.
- *
+ *
* @param {HTMLElement} li - The list item (li) element to check.
* @returns {boolean} - True if the list item is deletable, otherwise false.
*/
@@ -721,13 +760,13 @@ export const stack_sortable = class {
* @returns {void}
*/
_delete_all_from_used() {
- const lis = document.querySelectorAll('.usedList li[data-id]');
+ const lis = document.querySelectorAll(".usedList li[data-id]");
lis.forEach(li => {if (this._deletable_li(li)) {this._delete_li(li);}});
}
/**
* Deletes a list item (li) from its parent node.
- *
+ *
* @param {HTMLElement} li - The list item (li) element to delete.
*/
_delete_li(li) {
@@ -755,8 +794,7 @@ export const stack_sortable = class {
}
/**
- * TODO: fix this, it should not be an index or grid-item or grid-item-rigid
- * Check if an HTML element is double-clickable (i.e., it is not a header element).
+ * Check if an HTML element is double-clickable (i.e., it is not a header or index element).
*
* This private method is called on items inside the used or available list.
*
@@ -766,22 +804,33 @@ export const stack_sortable = class {
* @returns {boolean} - Returns true if the element is double-clickable, false otherwise.
*/
_double_clickable(item) {
- return !item.matches(".header");
+ return !item.matches(".header") && !item.matches(".index");
}
- /* TODO : simplify this */
+ /**
+ * Flips the question between vertical and horizontal orientations.
+ *
+ * @method
+ * @returns {void}
+ */
_flip_orientation() {
+ // Define CSS classes based on orientation and whether using for proof or grouping or matching.
var addClass = (this.orientation === "row") ? ["list-group", "col"] : ["row"];
if (this.grid) {
+ // Current classes we need to remove.
var removeClass = (this.orientation === "row") ? ["list-group", "row"] : ["list-group", "col"];
+ // Current grid class being used.
var currGridClass = (this.orientation === "row") ? "grid-item-rigid" : "grid-item";
+ // Grid class to add.
var gridAddClass = (this.orientation === "row") ? "grid-item" : "grid-item-rigid"
+ // Get all grid items and replace their classes
var gridItems = document.querySelectorAll(`.${currGridClass}`);
gridItems.forEach((item) => {
item.classList.remove(currGridClass);
item.classList.add(gridAddClass);
})
+ // In matching mode, we need to add rigid styles to columns as well as items.
if (this.rows !== "") {
[].concat(...this.used).forEach((div) => {
if (this.orientation === "col") {
@@ -794,8 +843,11 @@ export const stack_sortable = class {
})
}
} else {
+ // In proof mode just switch between row and col.
var removeClass = (this.orientation === "row") ? ["row"] : ["col"];
}
+
+ // Now classes have been defined appropriately according to case, we replace classes in answer lists.
this.colIds.forEach((colId) => {
var ul = document.getElementById(colId);
ul.classList.remove(...removeClass);
@@ -803,15 +855,20 @@ export const stack_sortable = class {
}
);
+ // Do the same for the available list.
this.available.classList.remove(...removeClass);
this.available.classList.add(...addClass);
+
+ // Move position of available list to above in horizontal, or to the right in vertical.
if (this.orientation === "col") {
this.available.parentNode.insertBefore(this.available, this.available.parentNode.firstChild);
} else {
this.available.parentNode.append(this.available);
}
+ // In grid mode (either matching or grouping) headers become indices and vice versa.
if (this.grid) {
+ // Headers to index
if (this.orientation === "col") {
document.querySelectorAll(".header").forEach((header) => {
if (!header.classList.contains("index")) {
@@ -827,34 +884,37 @@ export const stack_sortable = class {
}
})
}
- };
- if (this.use_index) {
- var indexDiv = document.getElementById("index");
- indexDiv.classList.remove(...removeClass);
- indexDiv.classList.add(...addClass);
- if (this.orientation === "col") {
- document.querySelectorAll("#index > .index").forEach((idx) => {
- if (!idx.classList.contains("header")) {
- idx.classList.remove("index");
- idx.classList.add("header");
- }
- })
- } else {
- document.querySelectorAll('#index > .header').forEach((header) => {
- if (!header.classList.contains("index")) {
- header.classList.remove("header");
- header.classList.add("index");
+ // Index to headers (if index is being used).
+ if (this.use_index) {
+ var indexDiv = document.getElementById("index");
+ indexDiv.classList.remove(...removeClass);
+ indexDiv.classList.add(...addClass);
+ if (this.orientation === "col") {
+ document.querySelectorAll("#index > .index").forEach((idx) => {
+ if (!idx.classList.contains("header")) {
+ idx.classList.remove("index");
+ idx.classList.add("header");
+ }
+ })
+ } else {
+ document.querySelectorAll("#index > .header").forEach((header) => {
+ if (!header.classList.contains("index")) {
+ header.classList.remove("header");
+ header.classList.add("index");
+ }
+ })
}
- })
- }
- }
+ }
+ };
+
+ // Keep track of current orientation.
this.orientation = (this.orientation === "row") ? "col" : "row";
}
/**
* Generates the initial state of used and available items based on the provided steps, input ID,
- * and number of columns and rows used. The shape of the used state will be `(1, 1, ?)` if in proof
+ * and number of columns and rows used. The shape of the used state will be `(1, 1, ?)` if in proof
* mode, `(n, 1, ?)` if `n` columns are specified and `(n, m, 1)` if `n` columns and `m` rows are specified.
*
* @method
@@ -864,8 +924,8 @@ export const stack_sortable = class {
* @returns {Object} The initial state object with used and available lists.
*/
_generate_state(steps, inputId, columns, rows) {
- const usedState = (rows === 0 || columns === 0) ?
- Array(columns).fill().map(() => [[]]) :
+ const usedState = (rows === 0 || columns === 0) ?
+ Array(columns).fill().map(() => [[]]) :
Array(columns).fill().map(() => Array(rows).fill([]));
let stateStore = document.getElementById(inputId);
if (stateStore === null) {
@@ -899,12 +959,12 @@ export const stack_sortable = class {
/**
* Checks if a list item (li) element is empty.
- *
+ *
* @param {HTMLElement} li - The list item (li) element to check.
* @returns {boolean} - True if the list item is empty, otherwise false.
*/
_is_empty_li(li) {
- return li.textContent.trim() === '' && li.children.length === 0;
+ return li.textContent.trim() === "" && li.children.length === 0;
}
/**
@@ -923,48 +983,48 @@ export const stack_sortable = class {
var group_val = {};
group_val.used = (this.rows === "") ?
{
- name: "sortableUsed",
- pull: true,
+ name: "sortableUsed",
+ pull: true,
put: true
- } :
+ } :
{
- name: "sortableUsed",
- pull: true,
- put: (to) => to.el.children.length < 1
+ name: "sortableUsed",
+ pull: true,
+ put: (to) => to.el.children.length < 1
};
-
+
group_val.available = (this.clone === "true") ?
{
- name: "sortableAvailable",
- pull: "clone",
- revertClone: true,
+ name: "sortableAvailable",
+ pull: "clone",
+ revertClone: true,
put: false
} :
{
- name: "sortableAvailable",
+ name: "sortableAvailable",
put: true
};
-
+
var options_to_assign = this.submitted ?
{
used : {
- ghostClass: "list-group-item-info",
- group: group_val.used,
+ ghostClass: "list-group-item-info",
+ group: group_val.used,
disabled: true
- },
+ },
available : {
- ghostClass: "list-group-item-info",
- group: group_val.available,
+ ghostClass: "list-group-item-info",
+ group: group_val.available,
disabled: true
}
- } :
+ } :
{
used : {
- ghostClass: "list-group-item-info",
+ ghostClass: "list-group-item-info",
group: group_val.used
- },
+ },
available : {
- ghostClass: "list-group-item-info",
+ ghostClass: "list-group-item-info",
group: group_val.available
}
}
diff --git a/corsscripts/stacksortable.min.js b/corsscripts/stacksortable.min.js
index a858004880275ba3ab2a9866647b6fdb3a79ca9e..d4441c5f434dda336d5908ef9b8a62f3e8940c9e 100644
--- a/corsscripts/stacksortable.min.js
+++ b/corsscripts/stacksortable.min.js
@@ -6,57 +6,57 @@ index=steps["index"];steps=steps["steps"];}
if(typeof steps==="string"){steps=_stackstring_objectify(steps);}
return[steps,sortableUserOpts,headers,available_header,index,valid];}
function _stackstring_objectify(stackjson_array_string){return Object.fromEntries(new Map(Object.values(JSON.parse(stackjson_array_string))));}
-function _validate_parsons_JSON(steps){if(Object.values(steps).every((val)=>!(typeof(val)=='object'))){return _validate_flat_steps(steps);}
+function _validate_parsons_JSON(steps){if(Object.values(steps).every((val)=>!(typeof(val)=="object"))){return _validate_flat_steps(steps);}
if(Object.values(steps).some((val)=>typeof(val)=="object")){if(!_validate_top_level_keys_JSON(steps,["steps","options","headers","index","available_header"],["steps"])){return false;}
if(!_validate_flat_steps(steps["steps"])){return false;}
return true;}}
-function _validate_flat_steps(steps){if(typeof(steps)=='string'){steps=_stackstring_objectify(steps);}
-return Object.values(steps).every((val)=>typeof(val)=='string');}
-function _validate_top_level_keys_JSON(JSON,validKeys,requiredKeys){const keys=Object.keys(JSON);const missingRequiredKeys=requiredKeys.filter(key=>!keys.includes(key));const invalidKeys=keys.filter(key=>!validKeys.includes(key));return invalidKeys.length===0&&missingRequiredKeys.length===0;}
+function _validate_flat_steps(steps){if(typeof(steps)=="string"){steps=_stackstring_objectify(steps);}
+return Object.values(steps).every((val)=>typeof(val)=="string");}
+function _validate_top_level_keys_JSON(JSON,validKeys,requiredKeys){const keys=Object.keys(JSON);const missingRequiredKeys=requiredKeys.filter((key)=>!keys.includes(key));const invalidKeys=keys.filter((key)=>!validKeys.includes(key));return invalidKeys.length===0&&missingRequiredKeys.length===0;}
export function get_iframe_height(){return document.documentElement.offsetHeight;}
-export const stack_sortable=class{constructor(steps,inputId=null,options=null,clone=false,columns=1,rows=null,orientation="col",index="",grid=false,item_height=null,item_width=null){this.steps=steps;this.inputId=inputId;this.orientation=orientation;this.columns=(this.orientation==="col")?columns:rows;this.rows=(this.orientation==="col")?rows:columns;this.index=index;this.use_index=this.index!=="";this.grid=grid;this.item_class=this.grid?(this.orientation==="row"?"grid-item-rigid":"grid-item"):"list-group-item";this.item_height_width={'style':''};for(const[key,val]of[['height',item_height],['width',item_width]]){if(val!==''){this.item_height_width['style']+=`${key}:${val}px;`};};this.item_height_width=(this.item_height_width['style']==='')?{}:this.item_height_width;this.item_height=(item_height!=='')?{'style':`height:${item_height}px;`}:{};this.item_width=(item_width!=='')?{'style':`width:${item_width}px;`}:{};this.container_height_width=(this.item_height_width['style']!=='')?{'style':this.item_height_width['style']+'margin: 12px;'}:{};this.state=this._generate_state(this.steps,inputId,Number(this.columns),Number(this.rows));if(inputId!==null){this.input=document.getElementById(this.inputId);this.submitted=this.input.getAttribute("readonly")==="readonly"}
+export const stack_sortable=class stack_sortable{constructor(steps,inputId=null,options=null,clone=false,columns=1,rows=null,orientation="col",index="",grid=false,item_height=null,item_width=null){this.steps=steps;this.inputId=inputId;this.orientation=orientation;this.columns=((this.orientation==="col")?columns:rows);this.rows=((this.orientation==="col")?rows:columns);this.index=index;this.use_index=this.index!=="";this.grid=grid;this.item_class=(this.grid?(this.orientation==="row"?"grid-item-rigid":"grid-item"):"list-group-item");this.item_height_width={"style":""};for(const[key,val]of[["height",item_height],["width",item_width]]){if(val!==""){this.item_height_width["style"]+=`${key}:${val}px;`};};this.item_height_width=(this.item_height_width["style"]==="")?{}:this.item_height_width;this.container_height_width=(this.item_height_width["style"]!=="")?{"style":this.item_height_width["style"]+"margin: 12px;"}:{};this.state=this._generate_state(this.steps,inputId,Number(this.columns),Number(this.rows));if(inputId!==null){this.input=document.getElementById(this.inputId);this.submitted=this.input.getAttribute("readonly")==="readonly"}
this.ids=this._create_ids(this.rows,this.columns);this.availableId=this.ids.available;this.usedId=this.ids.used;this.clone=clone;this.defaultOptions={used:{animation:50,cancel:".header"},available:{animation:50,cancel:".header"}};this.userOptions=this._set_user_options(options);this.options=this._set_ghostClass_group_and_disabled_options();}
-add_dblclick_listeners(newUsed,newAvailable){this.available.addEventListener('dblclick',(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);li=(this.clone==="true")?li.cloneNode(true):this.available.removeChild(li);this.used[0][0].append(li);this.update_state(newUsed,newAvailable);}});this.used[0][0].addEventListener('dblclick',(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);this.used[0][0].removeChild(li);if(this.clone!=="true"){this.available.insertBefore(li,this.available.children[1]);}
+add_dblclick_listeners(newUsed,newAvailable){this.available.addEventListener("dblclick",(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);li=(this.clone==="true")?li.cloneNode(true):this.available.removeChild(li);this.used[0][0].append(li);this.update_state(newUsed,newAvailable);}});this.used[0][0].addEventListener("dblclick",(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);this.used[0][0].removeChild(li);if(this.clone!=="true"){this.available.insertBefore(li,this.available.children[1]);}
this.update_state(newUsed,newAvailable);}});}
-add_delete_all_listener(buttonId,newUsed,newAvailable){const button=document.getElementById(buttonId);button.addEventListener('click',()=>{this._delete_all_from_used();this.update_state(newUsed,newAvailable);});}
+add_delete_all_listener(buttonId,newUsed,newAvailable){const button=document.getElementById(buttonId);button.addEventListener("click",()=>{this._delete_all_from_used();this.update_state(newUsed,newAvailable);});}
add_headers(headers,available_header){for(const[i,value]of headers.entries()){var parentEl=document.getElementById(`usedList_${i}`);var header=this._create_header(value,`usedHeader_${i}`,this.item_height_width);parentEl.insertBefore(header,parentEl.firstChild);}
var parentEl=document.getElementById("availableList");parentEl.insertBefore(this._create_header(available_header,"availableHeader",this.item_height_width),parentEl.firstChild);}
add_index(index){for(const[i,value]of index.entries()){if(i===0){var idx=this._create_index(value,`usedIndex_${i}`,this.item_height_width);var addClass=this.orientation==="col"?"header":"index";idx.classList.add(addClass);}else{var idx=this._create_index(value,`usedIndex_${i}`,this.item_height_width);}
document.getElementById("index").append(idx);}}
add_reorientation_button(){var btn=document.createElement("button");btn.id="orientation";btn.setAttribute("class","parsons-button");var icon=document.createElement("i");icon.setAttribute("class","fa fa-refresh");btn.append(icon);btn.addEventListener("click",()=>this._flip_orientation());document.body.insertBefore(btn,document.getElementById("containerRow"));}
-create_row_col_divs(){var usedClassList=(!this.grid||this.orientation==="col")?["list-group",this.orientation,"usedList"]:[this.orientation,"usedList"];var itemClass=(this.orientation==="col")?"row":"col";var itemClassList=[itemClass,"usedList"];var availClassList=(!this.grid||this.orientation==="col")?["list-group",this.orientation]:[this.orientation];var container=document.getElementById("containerRow");if(this.use_index){var indexCol=document.createElement("div");indexCol.id="index";indexCol.classList.add(...usedClassList);container.append(indexCol);}
+create_row_col_divs(){var usedClassList=(!this.grid||this.orientation==="col")?(["list-group",this.orientation,"usedList"]):([this.orientation,"usedList"]);var itemClass=(this.orientation==="col")?"row":"col";var itemClassList=[itemClass,"usedList"];var availClassList=(!this.grid||this.orientation==="col")?["list-group",this.orientation]:[this.orientation];var container=document.getElementById("containerRow");if(this.use_index){var indexCol=document.createElement("div");indexCol.id="index";indexCol.classList.add(...usedClassList);container.append(indexCol);}
this.colIds.forEach((id)=>{var colDiv=document.createElement("ul");colDiv.id=id;colDiv.classList.add(...usedClassList);container.append(colDiv);});if(this.rows!==""){this.colIds.forEach((colId)=>{var colDiv=document.getElementById(colId);colDiv.classList.add("container");this.rowColIds[colId].forEach((rowColId)=>{var divRowCol=document.createElement("li");divRowCol.id=rowColId;divRowCol.classList.add(...itemClassList);colDiv.append(divRowCol);})})};var availDiv=document.createElement("ul");availDiv.id=this.ids.available;availDiv.classList.add(...availClassList);if(this.orientation==="col"){container.append(availDiv);}else{container.insertBefore(availDiv,container.firstChild);}
this.used=this.usedId.map(idList=>idList.map(id=>document.getElementById(id)));this.available=document.getElementById(this.availableId);}
generate_available(){this.state.available.forEach(key=>this.available.append(this._create_li(key,this.item_height_width)));}
generate_used(){for(const[i,value]of this.state.used.entries()){if(this.rows!==""&&this.columns!==""){for(const[j,val]of value.entries()){this._apply_attrs(this.used[i][j],this.container_height_width);val.forEach(key=>this.used[i][j].append(this._create_li(key,this.item_height_width)));}}else{value[0].forEach(key=>this.used[i][0].append(this._create_li(key,this.item_height_width)));}}}
-update_state(newUsed,newAvailable){var newState={used:newUsed.map((usedList)=>usedList.map((used)=>used.toArray())),available:newAvailable.toArray()};if(this.inputId!==null){this.input.value=JSON.stringify(newState);this.input.dispatchEvent(new Event('change'));}
+update_state(newUsed,newAvailable){var newState={used:newUsed.map((usedList)=>usedList.map((used)=>used.toArray())),available:newAvailable.toArray()};if(this.inputId!==null){this.input.value=JSON.stringify(newState);this.input.dispatchEvent(new Event("change"));}
this.state=newState;}
-validate_options(possibleOptionKeys,unknownErr,overwrittenErr){var err='';var keysRecognised=true;var invalidKeys=[];Object.keys(this.options.used).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});Object.keys(this.options.available).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});if(!keysRecognised){err+=unknownErr+invalidKeys.join(", ")+". ";}
+validate_options(possibleOptionKeys,unknownErr,overwrittenErr){var err="";var keysRecognised=true;var invalidKeys=[];Object.keys(this.options.used).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});Object.keys(this.options.available).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});if(!keysRecognised){err+=unknownErr+invalidKeys.join(", ")+". ";}
var overwrittenKeys=[];var keysPreserved=true;["ghostClass","group","onSort"].forEach(key=>{if(Object.keys(this.userOptions.used).includes(key)||Object.keys(this.userOptions.available).includes(key))
{keysPreserved=false;overwrittenKeys.push(key);}});if(!keysPreserved){err+=overwrittenErr+overwrittenKeys.join(", ")+".";}
if(!keysRecognised||!keysPreserved){this._display_warning(err);}}
_apply_attrs(el,opts){for(const[key,value]of Object.entries(opts)){el.setAttribute(key,value);}}
-_create_header(innerHTML,id,attrs){let i=document.createElement("i");i.innerHTML=innerHTML;var addClass=(this.orientation==="col")?[this.item_class,'header']:[this.item_class,'index'];i.classList.add(...addClass);this._apply_attrs(i,{...{"id":id},...attrs});return i;}
+_create_header(innerHTML,id,attrs){let i=document.createElement("i");i.innerHTML=innerHTML;var addClass=(this.orientation==="col")?[this.item_class,"header"]:[this.item_class,"index"];i.classList.add(...addClass);this._apply_attrs(i,{...{"id":id},...attrs});return i;}
_create_ids(rows,columns){var colIdx=Array.from({length:columns},(_,i)=>i);var rowIdx=Array.from({length:rows},(_,j)=>j);this.colIds=colIdx.map((idx)=>`usedList_${idx}`);this.rowColIds={}
colIdx.forEach((i)=>this.rowColIds[this.colIds[i]]=rowIdx.map((j)=>`usedList_${j}${i}`));var usedIds=(rows==="")?this.colIds.map((id)=>[id]):Object.values(this.rowColIds);return{used:usedIds,available:"availableList"};}
-_create_index(innerHTML,id,attrs){let i=document.createElement("i");i.innerHTML=innerHTML;var addClass=(this.orientation==="col")?[this.item_class,'index']:[this.item_class,'header'];i.classList.add(...addClass);this._apply_attrs(i,{...{"id":id},...attrs});return i;}
+_create_index(innerHTML,id,attrs){let i=document.createElement("i");i.innerHTML=innerHTML;var addClass=(this.orientation==="col")?[this.item_class,"index"]:[this.item_class,"header"];i.classList.add(...addClass);this._apply_attrs(i,{...{"id":id},...attrs});return i;}
_create_li(stepKey,attrs){let li=document.createElement("li");li.innerHTML=this.steps[stepKey];this._apply_attrs(li,{...{"data-id":stepKey},...attrs});li.className=this.item_class;return li;}
_deletable_li(li){return!li.matches(".header")&&!li.matches(".index")&&!this._is_empty_li(li);}
-_delete_all_from_used(){const lis=document.querySelectorAll('.usedList li[data-id]');lis.forEach(li=>{if(this._deletable_li(li)){this._delete_li(li);}});}
+_delete_all_from_used(){const lis=document.querySelectorAll(".usedList li[data-id]");lis.forEach(li=>{if(this._deletable_li(li)){this._delete_li(li);}});}
_delete_li(li){li.parentNode.removeChild(li);}
_display_warning(msg){var warning=document.createElement("div");warning.className="sortable-warning";var exclamation=document.createElement("i");exclamation.className="icon fa fa-exclamation-circle text-danger fa-fw";warning.append(exclamation);var warningMessage=document.createElement("span");warningMessage.textContent=msg;warning.append(warningMessage);document.body.insertBefore(warning,document.getElementById("sortableContainer"));}
-_double_clickable(item){return!item.matches(".header");}
+_double_clickable(item){return!item.matches(".header")&&!item.matches(".index");}
_flip_orientation(){var addClass=(this.orientation==="row")?["list-group","col"]:["row"];if(this.grid){var removeClass=(this.orientation==="row")?["list-group","row"]:["list-group","col"];var currGridClass=(this.orientation==="row")?"grid-item-rigid":"grid-item";var gridAddClass=(this.orientation==="row")?"grid-item":"grid-item-rigid"
var gridItems=document.querySelectorAll(`.${currGridClass}`);gridItems.forEach((item)=>{item.classList.remove(currGridClass);item.classList.add(gridAddClass);})
if(this.rows!==""){[].concat(...this.used).forEach((div)=>{if(this.orientation==="col"){div.classList.remove("row");div.classList.add("col","col-rigid");}else{div.classList.remove("col","col-rigid");div.classList.add("row");}})}}else{var removeClass=(this.orientation==="row")?["row"]:["col"];}
this.colIds.forEach((colId)=>{var ul=document.getElementById(colId);ul.classList.remove(...removeClass);ul.classList.add(...addClass);});this.available.classList.remove(...removeClass);this.available.classList.add(...addClass);if(this.orientation==="col"){this.available.parentNode.insertBefore(this.available,this.available.parentNode.firstChild);}else{this.available.parentNode.append(this.available);}
-if(this.grid){if(this.orientation==="col"){document.querySelectorAll(".header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}});}else{document.querySelectorAll(".index").forEach((index)=>{if(!index.classList.contains("header")){index.classList.remove("index");index.classList.add("header");}})}};if(this.use_index){var indexDiv=document.getElementById("index");indexDiv.classList.remove(...removeClass);indexDiv.classList.add(...addClass);if(this.orientation==="col"){document.querySelectorAll("#index > .index").forEach((idx)=>{if(!idx.classList.contains("header")){idx.classList.remove("index");idx.classList.add("header");}})}else{document.querySelectorAll('#index > .header').forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}})}}
-this.orientation=(this.orientation==="row")?"col":"row";}
+if(this.grid){if(this.orientation==="col"){document.querySelectorAll(".header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}});}else{document.querySelectorAll(".index").forEach((index)=>{if(!index.classList.contains("header")){index.classList.remove("index");index.classList.add("header");}})}
+if(this.use_index){var indexDiv=document.getElementById("index");indexDiv.classList.remove(...removeClass);indexDiv.classList.add(...addClass);if(this.orientation==="col"){document.querySelectorAll("#index > .index").forEach((idx)=>{if(!idx.classList.contains("header")){idx.classList.remove("index");idx.classList.add("header");}})}else{document.querySelectorAll("#index > .header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}})}}};this.orientation=(this.orientation==="row")?"col":"row";}
_generate_state(steps,inputId,columns,rows){const usedState=(rows===0||columns===0)?Array(columns).fill().map(()=>[[]]):Array(columns).fill().map(()=>Array(rows).fill([]));let stateStore=document.getElementById(inputId);if(stateStore===null){return{used:usedState,available:[...Object.keys(steps)]};}
return(stateStore.value&&stateStore.value!="")?JSON.parse(stateStore.value):{used:usedState,available:[...Object.keys(steps)]};}
_get_moveable_parent_li(target){var li=target;while(!li.matches(".list-group-item")){li=li.parentNode;}
return li;}
-_is_empty_li(li){return li.textContent.trim()===''&&li.children.length===0;}
+_is_empty_li(li){return li.textContent.trim()===""&&li.children.length===0;}
_set_ghostClass_group_and_disabled_options(){var group_val={};group_val.used=(this.rows==="")?{name:"sortableUsed",pull:true,put:true}:{name:"sortableUsed",pull:true,put:(to)=>to.el.children.length<1};group_val.available=(this.clone==="true")?{name:"sortableAvailable",pull:"clone",revertClone:true,put:false}:{name:"sortableAvailable",put:true};var options_to_assign=this.submitted?{used:{ghostClass:"list-group-item-info",group:group_val.used,disabled:true},available:{ghostClass:"list-group-item-info",group:group_val.available,disabled:true}}:{used:{ghostClass:"list-group-item-info",group:group_val.used},available:{ghostClass:"list-group-item-info",group:group_val.available}}
var options={used:Object.assign(Object.assign({},this.userOptions.used),options_to_assign.used),available:Object.assign(Object.assign({},this.userOptions.available),options_to_assign.available)};return options;}
_set_user_options(options){var userOptions;if(options===null){userOptions=this.defaultOptions;}else{userOptions={used:Object.assign(this.defaultOptions.used,options.used),available:Object.assign(this.defaultOptions.available,options.available)};}