Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
moodle-qtype_stack
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
elc
moodle-qtype_stack
Commits
5742babb
Commit
5742babb
authored
1 year ago
by
smmercuri
Browse files
Options
Downloads
Patches
Plain Diff
Tidy JS library
parent
86408ea9
Branches
Branches containing commit
No related tags found
No related merge requests found
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
corsscripts/stacksortable.js
+505
-509
505 additions, 509 deletions
corsscripts/stacksortable.js
with
505 additions
and
509 deletions
corsscripts/stacksortable.js
+
505
−
509
View file @
5742babb
...
@@ -25,74 +25,74 @@ export const SUPPORTED_CALLBACK_FUNCTIONS = [
...
@@ -25,74 +25,74 @@ export const SUPPORTED_CALLBACK_FUNCTIONS = [
];
];
/**
/**
* Preprocess and validate
proof
steps,
block
user options,
and sortable user option
s ready for use in `stack_sortable` class.
* Preprocess and validate steps,
sortable
user options,
headers and indice
s ready for use in `stack_sortable` class.
*
*
* The function takes
proof
steps in the form of a Parson's JSON or Maxima string variable, along with block user options
* The function takes steps in the form of a Parson's JSON or Maxima string variable, along with block user options
* and sortable user options. It performs the following tasks:
* and sortable user options. It performs the following tasks:
* 1. If `
proofS
teps` is a Maxima string of expected format, it converts it to an object using `_stackstring_objectify`.
* 1. If `
s
teps` is a Maxima string of expected format, it converts it to an object using `_stackstring_objectify`.
* 2. It validates the structure of the Parson's JSON using `_validate_parsons_JSON`.
* 2. It validates the structure of the Parson's JSON using `_validate_parsons_JSON`.
* 3. If the Parsons JSON contains "steps" and "options," it separates them.
* 3. If the Parsons JSON is of depth two with a valid set of top-level keys it separates them.
* - If "header" is present in options, it separates this away from Sortable options into `blockUserOpts`.
* 4. If `steps` is a Maxima string (after separation), it converts it to an object.
* - It splits Sortable options into "used" and "available" and passes to `sortableUserOpts`.
* 4. If `proofSteps` is a Maxima string (after separation), it converts it to an object.
*
*
* @param {
string|Object} proofSteps - The proof steps to be preprocessed. Either a JSON of expected format
* @param {
Object|string} steps - The steps object or string representation of steps.
*
or
*
@param {Object} sortableUserOpts - Options for the sortable plugin.
* @param {
Object} blockUserOpts - Block user options for the 'header' setting, should be passed as an empty Object
.
* @param {
Array} headers - Headers for the answer lists
.
* @param {
Object} sortableUserOpts - Sortable user options split into used and available, should be passed as an empty Objec
t.
* @param {
Array} available_header - Header for the available lis
t.
* @
returns
{Array}
- An array containing preprocessed proof steps, block user options,
* @
param
{Array}
index - Index column.
*
sortable user options, and a boolean indicat
in
g
th
e validity of the proof steps structure
.
*
@returns {Array} - An array containing preprocessed steps, options, headers, available header, and index
in th
at order
.
*
*
* @example
* @example
* // Returns [
* // Returns [
* // { step1: "Proof step 1", step2: "Proof step 2" },
* // { step1: "step 1 text", step2: "step 2 text" },
* // { used: { header: "Header 1" }, available: { header: "Header 2" } },
* // { option1: "Value 1", option2: "Value 2" },
* // { used: { option1: "Value 1" }, available: { option2: "Value 2" } },
* // ["header 1", "header 2"],
* // ["Drag from here:"],
* // null,
* // true
* // true
* // ]
* // ]
* preprocess_steps({
* preprocess_steps({
* steps: {
* steps: {
* step1: "
Proof
step 1",
* step1: "step 1
text
",
* step2: "
Proof
step 2"
* step2: "step 2
text
"
* },
* },
* options: {
* options: {
* header: ["Header 1", "Header 2"],
* option1: "Value 1",
* option1: "Value 1",
* option2: "Value 2"
* option2: "Value 2"
* }
* }
* },
{}, {}
);
* },
["header 1", "header 2"], ["Drag from here:"], null
);
*/
*/
export
function
preprocess_steps
(
proofS
teps
,
sortableUserOpts
,
headers
,
available_header
,
index
)
{
export
function
preprocess_steps
(
s
teps
,
sortableUserOpts
,
headers
,
available_header
,
index
)
{
// Check if
proofS
teps is a string and convert it to an object
// Check if
s
teps is a string and convert it to an object
// (this occurs when
proof
steps are a flat list coming from a Maxima variable)
// (this occurs when
the
steps are a flat list coming from a Maxima variable)
if
(
typeof
proofS
teps
===
"
string
"
)
{
if
(
typeof
s
teps
===
"
string
"
)
{
proofS
teps
=
_stackstring_objectify
(
proofS
teps
);
s
teps
=
_stackstring_objectify
(
s
teps
);
}
}
// Validate the object
// Validate the object
var
valid
=
_validate_parsons_JSON
(
proofS
teps
);
var
valid
=
_validate_parsons_JSON
(
s
teps
);
// At this point, we know proofSteps is either a flat JSON, or it's top-level keys are a subset of
// At this point, we know steps is either a flat JSON, or it's top-level keys are a subset of
// ["steps", "options", "headers", ""index"], and contains at least "steps". Separate these if they are present
// ["steps", "options", "headers", "available_header", "index"], and contains at least "steps".
if
(
_validate_top_level_keys_JSON
(
proofSteps
,
[
"
steps
"
,
"
options
"
,
"
headers
"
,
"
index
"
,
"
available_header
"
],
[
"
steps
"
]))
{
// Separate these if they are present.
var
sortableUserOpts
=
proofSteps
[
"
options
"
];
if
(
_validate_top_level_keys_JSON
(
steps
,
[
"
steps
"
,
"
options
"
,
"
headers
"
,
"
index
"
,
"
available_header
"
],
[
"
steps
"
]))
{
var
sortableUserOpts
=
steps
[
"
options
"
];
// only want to replace defaults for headers if they have been provided
// only want to replace defaults for headers if they have been provided
if
(
"
headers
"
in
proofS
teps
)
{
if
(
"
headers
"
in
s
teps
)
{
headers
=
proofS
teps
[
"
headers
"
];
headers
=
s
teps
[
"
headers
"
];
}
}
if
(
"
available_header
"
in
proofS
teps
)
{
if
(
"
available_header
"
in
s
teps
)
{
available_header
=
proofS
teps
[
"
available_header
"
];
available_header
=
s
teps
[
"
available_header
"
];
}
}
index
=
proofS
teps
[
"
index
"
];
index
=
s
teps
[
"
index
"
];
proofS
teps
=
proofS
teps
[
"
steps
"
];
s
teps
=
s
teps
[
"
steps
"
];
}
}
// Convert
proofS
teps to an object if it is still a string (occurs when the
proof
steps comes from a Maxima variable)
// Convert
s
teps to an object if it is still a string (occurs when the steps comes from a Maxima variable)
if
(
typeof
proofS
teps
===
"
string
"
)
{
if
(
typeof
s
teps
===
"
string
"
)
{
proofS
teps
=
_stackstring_objectify
(
proofS
teps
);
s
teps
=
_stackstring_objectify
(
s
teps
);
}
}
return
[
proofS
teps
,
sortableUserOpts
,
headers
,
available_header
,
index
,
valid
];
return
[
s
teps
,
sortableUserOpts
,
headers
,
available_header
,
index
,
valid
];
}
}
/**
/**
...
@@ -111,30 +111,31 @@ function _stackstring_objectify(stackjson_array_string) {
...
@@ -111,30 +111,31 @@ function _stackstring_objectify(stackjson_array_string) {
}
}
/**
/**
* Validate the structure of Parson's JSON
for proof
steps.
* Validate the structure of Parson's JSON
`
steps
`
.
*
*
* The function checks the structure of the provided Parson's JSON (`
proofS
teps`)
* The function checks the structure of the provided Parson's JSON (`
s
teps`)
* to ensure it follows specific patterns:
* to ensure it follows specific patterns:
* 1. If the JSON has depth 1, it should be a valid proofStep JSON (i.e., should have string values).
* 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 ["steps", "options"], and the value for "steps"
* 2. If the JSON has depth 2, the top-level keys should be a subset of
* should be a valid proofStep JSON. Options are not validated here.
* `["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}
proofS
teps - The Parson's JSON to be validated.
* @param {Object}
s
teps - The Parson's JSON to be validated.
* @returns {boolean} - Returns true if the provided Parsons JSON follows the expected structure, false otherwise.
* @returns {boolean} - Returns true if the provided Parsons JSON follows the expected structure, false otherwise.
*
*
* @example
* @example
* // Returns true
* // Returns true
* _validate_parsons_JSON({
* _validate_parsons_JSON({
* "step1": "
proof
step 1",
* "step1": "step 1
text
",
* "step2": "
proof
step 2"
* "step2": "step 2
text
"
* });
* });
*
*
* @example
* @example
* // Returns true
* // Returns true
* _validate_parsons_JSON({
* _validate_parsons_JSON({
* "steps": {
* "steps": {
* "step1": "
proof
step 1",
* "step1": "step 1
text
",
* "step2": "
proof
step 2"
* "step2": "step 2
text
"
* },
* },
* "options": {
* "options": {
* "option1": "value1",
* "option1": "value1",
...
@@ -146,72 +147,79 @@ function _stackstring_objectify(stackjson_array_string) {
...
@@ -146,72 +147,79 @@ function _stackstring_objectify(stackjson_array_string) {
* // Returns false
* // Returns false
* _validate_parsons_JSON({
* _validate_parsons_JSON({
* "invalidKey": {
* "invalidKey": {
* "step1": "
proof
step 1",
* "step1": "step 1
text
",
* "step2": "
proof
step 2"
* "step2": "step 2
text
"
* }
* }
* });
* });
*/
*/
function
_validate_parsons_JSON
(
proofSteps
)
{
function
_validate_parsons_JSON
(
steps
)
{
// If the JSON has depth 1 then it should be a valid proofStep JSON (i.e., should have string values)
// If the JSON has depth 1 then it should be a valid flat JSON (i.e., should have string values).
if
(
Object
.
values
(
proofSteps
).
every
((
val
)
=>
!
(
typeof
(
val
)
==
'
object
'
)))
{
if
(
Object
.
values
(
steps
).
every
((
val
)
=>
!
(
typeof
(
val
)
==
'
object
'
)))
{
return
_validate_proof_steps
(
proofSteps
);
return
_validate_flat_steps
(
steps
);
}
}
// Else the top-level of the JSON should have keys ["steps", "options"].
// Else the top-level of the JSON should have keys that are a subset of ["steps", "options", "headers", "index", "available_header"]
// The value for "keys" should be a valid proofStep JSON
// and a superset of ["steps"].
// We do not validate options here
// The value for "steps" should be a valid flat JSON.
if
(
Object
.
values
(
proofSteps
).
some
((
val
)
=>
typeof
(
val
)
==
"
object
"
))
{
// We do not validate options here.
if
(
JSON
.
stringify
(
Object
.
keys
(
proofSteps
))
!==
JSON
.
stringify
([
"
steps
"
,
"
options
"
]))
{
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
;
return
false
;
}
}
if
(
!
_validate_
proof
_steps
(
proofS
teps
[
"
steps
"
]))
{
if
(
!
_validate_
flat
_steps
(
s
teps
[
"
steps
"
]))
{
return
false
;
return
false
;
}
}
return
true
;
return
true
;
}
}
// TO-DO : we are missing one case here in depth 2 case and unclear how to catch it:
// if an author writes {"any string" : {#stackjson_stringify(proof_steps)#}},
// then this should throw an error
}
}
/**
/**
* Validate the structure of
proof
steps.
* Validate the structure of
a flat
steps
JSON
.
*
*
* The function checks the structure of the provided
proof
steps (`
proofS
teps`)
* The function checks the structure of the provided steps (`
s
teps`)
* to ensure that all values are strings.
* to ensure that all values are strings.
*
*
* If the
proof
steps are provided as a Maxima variable (string of form '[["key", "value"], ...]'), they are converted
* If the steps are provided as a Maxima variable (string of form '[["key", "value"], ...]'), they are converted
* to a JSON object using the `_stackstring_objectify` function before validation.
* to a JSON object using the `_stackstring_objectify` function before validation.
*
*
* @param {string|Object}
proofS
teps - The
proof steps
to be validated. If a string,
* @param {string|Object}
s
teps - The
flat JSON
to be validated. If a string,
* it is assumed to be a Maxima variable and will be converted to a JSON object.
* it is assumed to be a Maxima variable and will be converted to a JSON object.
* @returns {boolean} - Returns true if all values in
the proof
steps are strings, false otherwise.
* @returns {boolean} - Returns true if all values in
`
steps
`
are strings, false otherwise.
*
*
* @example
* @example
* // Returns true
* // Returns true
* _validate_
proof
_steps({
* _validate_
flat
_steps({
* step1: "
Proof
step 1",
* step1: "step 1
text
",
* step2: "
Proof
step 2"
* step2: "step 2
text
"
* });
* });
*
*
* @example
* @example
* // Returns true
* // Returns true
* _validate_
proof
_steps('["step1", "
Proof
step 1"], ["step2", "
Proof
step 2"]]');
* _validate_
flat
_steps('["step1", "step 1
text
"], ["step2", "step 2
text
"]]');
*
*
* @example
* @example
* // Returns false
* // Returns false
* _validate_
proof
_steps({
* _validate_
flat
_steps({
* step1: "
Proof
step 1",
* step1: "step 1
text
",
* step2: 123 // Not a string
* step2: 123 // Not a string
* });
* });
*/
*/
function
_validate_
proof
_steps
(
proofS
teps
)
{
function
_validate_
flat
_steps
(
s
teps
)
{
// Case when
proof
steps are coming from a Maxima variable: convert to a JSON
// Case when steps are coming from a Maxima variable: convert to a JSON
if
(
typeof
(
proofS
teps
)
==
'
string
'
)
{
if
(
typeof
(
s
teps
)
==
'
string
'
)
{
proofS
teps
=
_stackstring_objectify
(
proofS
teps
);
s
teps
=
_stackstring_objectify
(
s
teps
);
}
}
return
Object
.
values
(
proofS
teps
).
every
((
val
)
=>
typeof
(
val
)
==
'
string
'
);
return
Object
.
values
(
s
teps
).
every
((
val
)
=>
typeof
(
val
)
==
'
string
'
);
}
}
/**
* 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.
* @returns {boolean} - True if the JSON object passes validation, otherwise false.
*/
function
_validate_top_level_keys_JSON
(
JSON
,
validKeys
,
requiredKeys
)
{
function
_validate_top_level_keys_JSON
(
JSON
,
validKeys
,
requiredKeys
)
{
const
keys
=
Object
.
keys
(
JSON
);
const
keys
=
Object
.
keys
(
JSON
);
const
missingRequiredKeys
=
requiredKeys
.
filter
(
key
=>
!
keys
.
includes
(
key
));
const
missingRequiredKeys
=
requiredKeys
.
filter
(
key
=>
!
keys
.
includes
(
key
));
...
@@ -219,57 +227,6 @@ function _validate_top_level_keys_JSON(JSON, validKeys, requiredKeys) {
...
@@ -219,57 +227,6 @@ function _validate_top_level_keys_JSON(JSON, validKeys, requiredKeys) {
return
invalidKeys
.
length
===
0
&&
missingRequiredKeys
.
length
===
0
;
return
invalidKeys
.
length
===
0
&&
missingRequiredKeys
.
length
===
0
;
}
}
/**
* Flips the orientation of specified used and available lists, and the bin (if present) in the UI.
* The function toggles between 'list-group row' and 'list-group col' classes for the specified elements.
* The bin element (if present) is expected to have ID 'bin'.
*
* @param {string} usedId - The ID of the used list element.
* @param {string} availableId - The ID of the available list element.
* @returns {void}
*
* @example
* // HTML structure:
* // <div id="usedList" class="list-group row">...</div>
* // <div id="availableList" class="list-group row">...</div>
* // <div id="bin" class="list-group row">...</div>
*
* // JavaScript usage:
* _flip_orientation('usedList', 'availableList');
*/
function
_flip_orientation
(
usedId
,
availableId
)
{
var
usedList
=
document
.
getElementById
(
usedId
);
var
availableList
=
document
.
getElementById
(
availableId
);
var
newClass
=
usedList
.
className
==
'
list-group row
'
?
'
list-group col
'
:
'
list-group row
'
;
usedList
.
setAttribute
(
'
class
'
,
newClass
);
availableList
.
setAttribute
(
'
class
'
,
newClass
);
}
/**
* Adds an event listener to a button element with the specified ID to trigger the flipping
* of orientation between 'list-group row' and 'list-group col' classes for specified UI elements.
* This event will also change the orientation of the bin element (if present), which is expected
* to have ID 'bin'.
*
* @param {string} buttonId - The ID of the button element to which the event listener is added.
* @param {string} usedId - The ID of the used list element.
* @param {string} availableId - The ID of the available list element.
* @returns {void}
*
* @example
* // HTML structure:
* // <button id="toggleButton">Toggle Orientation</button>
* // <div id="usedList" class="list-group row">...</div>
* // <div id="availableList" class="list-group row">...</div>
*
* // JavaScript usage:
* add_orientation_listener('toggleButton', 'usedList', 'availableList');
*/
export
function
add_orientation_listener
(
buttonId
,
usedId
,
availableId
)
{
const
button
=
document
.
getElementById
(
buttonId
);
button
.
addEventListener
(
'
click
'
,
()
=>
_flip_orientation
(
usedId
,
availableId
));
}
/**
/**
* Get the current height of the iframe's content document.
* Get the current height of the iframe's content document.
*
*
...
@@ -279,22 +236,18 @@ export function get_iframe_height() {
...
@@ -279,22 +236,18 @@ export function get_iframe_height() {
return
document
.
documentElement
.
offsetHeight
;
return
document
.
documentElement
.
offsetHeight
;
}
}
function
_is_empty_li
(
li
)
{
return
li
.
textContent
.
trim
()
===
''
&&
li
.
children
.
length
===
0
;
}
/**
/**
* Class for for managing Sortable lists for Parson's
proof
questions in STACK.
* Class for for managing Sortable lists for Parson's
block
questions in STACK.
*
*
* @class
* @class
* @param {Object}
proofS
teps - Object containing
proof
steps.
* @param {Object}
s
teps - Object containing
flat
steps
JSON
.
* @param {string} availableId - ID of the available list element.
* @param {string} availableId - ID of the available list element.
* @param {string} usedId - ID of the used 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 {string|null} inputId - ID of the input element for storing state (optional).
* @param {Object|null} options - Custom options for sortable lists (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 {boolean} clone - Flag indicating whether to clone elements during drag-and-drop.
*
*
* @property {Object}
proofS
teps - Object containing
proof
steps.
* @property {Object}
s
teps - Object containing
all
steps.
* @property {string} inputId - ID of the input element for storing state (optional).
* @property {string} inputId - ID of the input element for storing state (optional).
* @property {Object} state - Current state of used and available items.
* @property {Object} state - Current state of used and available items.
* @property {Object} userOptions - User-defined options merged with default options.
* @property {Object} userOptions - User-defined options merged with default options.
...
@@ -334,7 +287,7 @@ export const stack_sortable = class {
...
@@ -334,7 +287,7 @@ export const stack_sortable = class {
*
Constructor
for
the
StackSortable
class
.
*
Constructor
for
the
StackSortable
class
.
*
*
*
@
constructor
*
@
constructor
*
@
param
{
Object
}
proofS
teps
-
Object
containing
proof
steps
.
*
@
param
{
Object
}
s
teps
-
Object
containing
the
flat
steps
JSON
.
*
@
param
{
string
}
availableId
-
ID
of
the
available
list
element
.
*
@
param
{
string
}
availableId
-
ID
of
the
available
list
element
.
*
@
param
{
string
}
usedId
-
ID
of
the
used
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
{
string
|
null
}
inputId
-
ID
of
the
input
element
for
storing
state
(
optional
).
...
@@ -342,11 +295,18 @@ export const stack_sortable = class {
...
@@ -342,11 +295,18 @@ export const stack_sortable = class {
*
of
form
{
used
:
UsedOptions
,
available
:
AvailableOptions
}
(
optional
).
*
of
form
{
used
:
UsedOptions
,
available
:
AvailableOptions
}
(
optional
).
*
@
param
{
boolean
}
clone
-
Flag
indicating
whether
to
clone
elements
during
sorting
.
*
@
param
{
boolean
}
clone
-
Flag
indicating
whether
to
clone
elements
during
sorting
.
*
/
*
/
constructor
(
steps
,
// TODO : add containerId as param
inputId
=
null
,
// TODO : be careful with default parameters, these should all be strings
options
=
null
,
constructor
(
proofSteps
,
inputId
=
null
,
options
=
null
,
clone
=
false
,
columns
=
1
,
rows
=
null
,
orientation
=
"
col
"
,
index
=
""
,
grid
=
false
,
item_height
=
null
,
item_width
=
null
)
{
clone
=
false
,
this
.
proofSteps
=
proofSteps
;
columns
=
1
,
rows
=
null
,
orientation
=
"
col
"
,
index
=
""
,
grid
=
false
,
item_height
=
null
,
item_width
=
null
)
{
this
.
steps
=
steps
;
this
.
inputId
=
inputId
;
this
.
inputId
=
inputId
;
this
.
orientation
=
orientation
;
this
.
orientation
=
orientation
;
this
.
columns
=
(
this
.
orientation
===
"
col
"
)
?
columns
:
rows
;
this
.
columns
=
(
this
.
orientation
===
"
col
"
)
?
columns
:
rows
;
...
@@ -363,22 +323,17 @@ export const stack_sortable = class {
...
@@ -363,22 +323,17 @@ export const stack_sortable = class {
this
.
item_height_width
=
(
this
.
item_height_width
[
'
style
'
]
===
''
)
?
{}
:
this
.
item_height_width
;
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_height
=
(
item_height
!==
''
)
?
{
'
style
'
:
`height:
${
item_height
}
px;`
}
:
{};
this
.
item_width
=
(
item_width
!==
''
)
?
{
'
style
'
:
`width:
${
item_width
}
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
.
container_height_width
=
(
this
.
item_height_width
[
'
style
'
]
!==
''
)
?
{
'
style
'
:
this
.
item_height_width
[
'
style
'
]
+
'
margin: 12px;
'
}
:
{};
this
.
state
=
this
.
_generate_state
(
this
.
proofS
teps
,
inputId
,
Number
(
this
.
columns
),
Number
(
this
.
rows
));
this
.
state
=
this
.
_generate_state
(
this
.
s
teps
,
inputId
,
Number
(
this
.
columns
),
Number
(
this
.
rows
));
if
(
inputId
!==
null
)
{
if
(
inputId
!==
null
)
{
this
.
input
=
document
.
getElementById
(
this
.
inputId
);
this
.
input
=
document
.
getElementById
(
this
.
inputId
);
this
.
submitted
=
this
.
input
.
getAttribute
(
"
readonly
"
)
===
"
readonly
"
this
.
submitted
=
this
.
input
.
getAttribute
(
"
readonly
"
)
===
"
readonly
"
}
}
this
.
ids
=
this
.
_create_ids
(
this
.
rows
,
this
.
columns
);
this
.
ids
=
this
.
_create_ids
(
this
.
rows
,
this
.
columns
);
this
.
availableId
=
this
.
ids
.
available
;
this
.
availableId
=
this
.
ids
.
available
;
//this.available = document.getElementById(this.availableId);
this
.
usedId
=
this
.
ids
.
used
;
this
.
usedId
=
this
.
ids
.
used
;
//this.used = this.usedId.map(idList => idList.map(id => document.getElementById(id)));
//this.used = document.getElementById(this.usedId);
this
.
clone
=
clone
;
this
.
clone
=
clone
;
// TODO : additional default options?
this
.
defaultOptions
=
{
used
:
{
animation
:
50
,
cancel
:
"
.header
"
},
available
:
{
animation
:
50
,
cancel
:
"
.header
"
}};
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
// Merges user options and default, overwriting default with user options if they clash
this
.
userOptions
=
this
.
_set_user_options
(
options
);
this
.
userOptions
=
this
.
_set_user_options
(
options
);
...
@@ -389,22 +344,118 @@ export const stack_sortable = class {
...
@@ -389,22 +344,118 @@ export const stack_sortable = class {
this
.
options
=
this
.
_set_ghostClass_group_and_disabled_options
();
this
.
options
=
this
.
_set_ghostClass_group_and_disabled_options
();
}
}
_create_ids
(
rows
,
columns
)
{
/**
var
colIdx
=
Array
.
from
({
length
:
columns
},
(
_
,
i
)
=>
i
);
* Adds double-click listeners to move items upon double-click and updates the state accordingly.
var
rowIdx
=
Array
.
from
({
length
:
rows
},
(
_
,
j
)
=>
j
);
* Only supported for proofmode
this
.
colIds
=
colIdx
.
map
((
idx
)
=>
`usedList_
${
idx
}
`
);
* TODO : fix this
this
.
rowColIds
=
{}
*
colIdx
.
forEach
((
i
)
=>
this
.
rowColIds
[
this
.
colIds
[
i
]]
=
rowIdx
.
map
((
j
)
=>
`usedList_
${
j
}${
i
}
`
));
* @method
var
usedIds
=
(
rows
===
""
)
?
* @param {Object} newUsed - Updated used list.
this
.
colIds
.
map
((
id
)
=>
[
id
])
:
* @param {Object} newAvailable - Updated available list.
Object
.
values
(
this
.
rowColIds
);
* @returns {void}
*/
add_dblclick_listeners
(
newUsed
,
newAvailable
)
{
this
.
available
.
addEventListener
(
'
dblclick
'
,
(
e
)
=>
{
if
(
this
.
_double_clickable
(
e
.
target
))
{
// get highest-level parent
var
li
=
this
.
_get_moveable_parent_li
(
e
.
target
);
li
=
(
this
.
clone
===
"
true
"
)
?
li
.
cloneNode
(
true
)
:
this
.
available
.
removeChild
(
li
);
this
.
used
[
0
].
append
(
li
);
this
.
update_state
(
newUsed
,
newAvailable
);
}
});
this
.
used
[
0
].
addEventListener
(
'
dblclick
'
,
(
e
)
=>
{
if
(
this
.
_double_clickable
(
e
.
target
))
{
// get highest-level parent
var
li
=
this
.
_get_moveable_parent_li
(
e
.
target
);
this
.
used
[
0
].
removeChild
(
li
);
if
(
this
.
clone
!==
"
true
"
)
{
this
.
available
.
insertBefore
(
li
,
this
.
available
.
children
[
1
]);
}
this
.
update_state
(
newUsed
,
newAvailable
);
}
});
}
return
{
/**
used
:
usedIds
,
* Add a click event listener to a button to delete all items from the "used" list and
available
:
"
availableList
"
* updates the state accordingly.
};
*
* @method
* @param {string} buttonId - ID of the button element to attach the listener.
* @param {Object} newUsed - Updated "used" list.
* @param {Object} newAvailable - Updated "available" list.
* @returns {void}
*/
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
);});
}
/**
* Adds header elements to the used and available lists.
*
* @method
* @param {Object} headers - Object containing header text for used and available lists.
* @returns {void}
*/
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
);
}
/**
* 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
)
{
for
(
const
[
i
,
value
]
of
index
.
entries
())
{
// Deal with the item in both header and index separately
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
);
}
}
/**
* Adds a reorientation button to the document body.
*
* The button allows users to change the orientation of sortable lists between vertical
* and horizontal.
*/
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
"
));
}
}
/**
* 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
* also contain `"list-group"`.
* - Items class depends only on the orientation.
*/
create_row_col_divs
()
{
create_row_col_divs
()
{
var
usedClassList
=
(
!
this
.
grid
||
this
.
orientation
===
"
col
"
)
?
var
usedClassList
=
(
!
this
.
grid
||
this
.
orientation
===
"
col
"
)
?
[
"
list-group
"
,
this
.
orientation
,
"
usedList
"
]:
[
"
list-group
"
,
this
.
orientation
,
"
usedList
"
]:
...
@@ -456,121 +507,51 @@ export const stack_sortable = class {
...
@@ -456,121 +507,51 @@ export const stack_sortable = class {
this
.
available
=
document
.
getElementById
(
this
.
availableId
);
this
.
available
=
document
.
getElementById
(
this
.
availableId
);
}
}
_flip_orientation
()
{
/**
var
addClass
=
(
this
.
orientation
===
"
row
"
)
?
[
"
list-group
"
,
"
col
"
]
:
[
"
row
"
];
* Generates the available list based on the current state.
if
(
this
.
grid
)
{
*
var
removeClass
=
(
this
.
orientation
===
"
row
"
)
?
[
"
list-group
"
,
"
row
"
]
:
[
"
list-group
"
,
"
col
"
];
* @method
var
currGridClass
=
(
this
.
orientation
===
"
row
"
)
?
"
grid-item-rigid
"
:
"
grid-item
"
;
* @returns {void}
var
gridAddClass
=
(
this
.
orientation
===
"
row
"
)
?
"
grid-item
"
:
"
grid-item-rigid
"
*/
var
gridItems
=
document
.
querySelectorAll
(
`.
${
currGridClass
}
`
);
generate_available
()
{
gridItems
.
forEach
((
item
)
=>
{
this
.
state
.
available
.
forEach
(
key
=>
this
.
available
.
append
(
this
.
_create_li
(
key
,
this
.
item_height_width
)));
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
"
);
* Generates the used list based on the current state.
indexDiv
.
classList
.
remove
(...
removeClass
);
*
indexDiv
.
classList
.
add
(...
addClass
);
* @method
if
(
this
.
orientation
===
"
col
"
)
{
* @returns {void}
document
.
querySelectorAll
(
"
#index > .index
"
).
forEach
((
idx
)
=>
{
*/
if
(
!
idx
.
classList
.
contains
(
"
header
"
))
{
generate_used
()
{
idx
.
classList
.
remove
(
"
index
"
);
for
(
const
[
i
,
value
]
of
this
.
state
.
used
.
entries
())
{
idx
.
classList
.
add
(
"
header
"
);
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
{
}
else
{
document
.
querySelectorAll
(
'
#index > .header
'
).
forEach
((
header
)
=>
{
value
[
0
].
forEach
(
key
=>
this
.
used
[
i
][
0
].
append
(
this
.
_create_li
(
key
,
this
.
item_height_width
)));
if
(
!
header
.
classList
.
contains
(
"
index
"
))
{
header
.
classList
.
remove
(
"
header
"
);
header
.
classList
.
add
(
"
index
"
);
}
})
}
}
}
}
this
.
orientation
=
(
this
.
orientation
===
"
row
"
)
?
"
col
"
:
"
row
"
;
/*var usedList = document.getElementById(usedId);
var availableList = document.getElementById(availableId);
var newClass = usedList.className == 'list-group row' ? 'list-group col' : 'list-group row';
usedList.setAttribute('class', newClass);
availableList.setAttribute('class', newClass);*/
}
}
add_reorientation_button
()
{
/**
var
btn
=
document
.
createElement
(
"
button
"
);
* Updates the state based on changes in the used and available lists.
btn
.
id
=
"
orientation
"
;
*
btn
.
setAttribute
(
"
class
"
,
"
parsons-button
"
);
* @method
var
icon
=
document
.
createElement
(
"
i
"
);
* @param {Object} newUsed - Updated used list.
icon
.
setAttribute
(
"
class
"
,
"
fa fa-refresh
"
);
* @param {Object} newAvailable - Updated available list.
btn
.
append
(
icon
);
* @returns {void}
btn
.
addEventListener
(
"
click
"
,
()
=>
this
.
_flip_orientation
());
*/
document
.
body
.
insertBefore
(
btn
,
document
.
getElementById
(
"
containerRow
"
));
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
'
));
}
}
/*if (orientation === "row") {
this
.
state
=
newState
;
var availableEl = document.getElementById("availableList");
availableEl.classList.remove("list-group", "col");
availableEl.classList.add(orientation);
availableEl.parentNode.insertBefore(availableEl, availableList.parentNode.firstChild);
//document.getElementById("availableList").classList.add(orientation);
ids.used.forEach((idList) => {
document.getElementById(idList[0]).classList.remove("list-group", "col");
document.getElementById(idList[0]).classList.add(orientation);
})
if (use_index) {
var indexEl = document.getElementById("index");
indexEl.classList.remove("list-group", "col");
indexEl.classList.add("row");
}
}
};*/
/**
/**
* Validate user options against a list of possible option keys.
* Validate user options against a list of possible option keys.
...
@@ -632,195 +613,299 @@ export const stack_sortable = class {
...
@@ -632,195 +613,299 @@ export const stack_sortable = class {
}
}
/**
/**
*
Generates the available list based on the current state
.
*
Applies attributes to an HTML element
.
*
*
* @
method
* @
param {HTMLElement} el - The HTML element to which attributes will be applied.
* @
returns {void}
* @
param {Object} opts - An object containing attribute-value pairs to be applied.
*/
*/
generate_available
()
{
_apply_attrs
(
el
,
opts
)
{
this
.
state
.
available
.
forEach
(
key
=>
this
.
available
.
append
(
this
.
_create_li
(
key
,
this
.
item_height_width
)));
for
(
const
[
key
,
value
]
of
Object
.
entries
(
opts
))
{
}
el
.
setAttribute
(
key
,
value
);
_add_index
(
index
,
indexDOM
)
{
for
(
const
[
i
,
value
]
of
index
.
entries
())
{
indexDOM
.
append
(
this
.
_create_index
(
value
,
`usedIndex
${
i
}
`
,
this
.
item_height_width
));
}
}
}
}
/**
/**
*
Generates the used list based on the current sta
te.
*
Creates a header element with specified inner HTML, ID, and other attribu
te
s
.
*
*
* @method
* @param {string} innerHTML - The inner HTML content of the header element.
* @returns {void}
* @param {string} id - The ID attribute of the header element.
* @param {Object} attrs - An object containing additional attributes for the header element.
* @returns {HTMLElement} - The created header element.
*/
*/
generate_used
()
{
_create_header
(
innerHTML
,
id
,
attrs
)
{
for
(
const
[
i
,
value
]
of
this
.
state
.
used
.
entries
())
{
let
i
=
document
.
createElement
(
"
i
"
);
/*if (i === 0 && this.index !== null) {
i
.
innerHTML
=
innerHTML
;
this._add_index(this.index, value[0]);
var
addClass
=
(
this
.
orientation
===
"
col
"
)
?
}*/
[
this
.
item_class
,
'
header
'
]
:
[
this
.
item_class
,
'
index
'
];
if
(
this
.
rows
!==
""
&&
this
.
columns
!==
""
)
{
i
.
classList
.
add
(...
addClass
);
for
(
const
[
j
,
val
]
of
value
.
entries
())
{
this
.
_apply_attrs
(
i
,
{...{
"
id
"
:
id
},
...
attrs
});
this
.
_apply_attrs
(
this
.
used
[
i
][
j
],
this
.
container_height_width
);
return
i
;
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
)));
}
}
}
/**
* 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
* `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.
*/
_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
"
};
}
}
add_index
(
index
)
{
/**
for
(
const
[
i
,
value
]
of
index
.
entries
())
{
* Creates an index element with specified inner HTML, ID, and additional attributes.
if
(
i
===
0
)
{
*
var
idx
=
this
.
_create_index
(
value
,
`usedIndex_
${
i
}
`
,
this
.
item_height_width
);
* @param {string} innerHTML - The inner HTML content of the index element.
var
addClass
=
this
.
orientation
===
"
col
"
?
"
header
"
:
"
index
"
;
* @param {string} id - The ID attribute of the index element.
idx
.
classList
.
add
(
addClass
);
* @param {Object} attrs - An object containing additional attributes for the index element.
}
else
{
* @returns {HTMLElement} - The created index element.
var
idx
=
this
.
_create_index
(
value
,
`usedIndex_
${
i
}
`
,
this
.
item_height_width
);
*/
_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
;
}
}
document
.
getElementById
(
"
index
"
).
append
(
idx
);
/**
* 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.
*/
_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
;
}
}
/**
* 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.
*/
_deletable_li
(
li
)
{
return
!
li
.
matches
(
"
.header
"
)
&&
!
li
.
matches
(
"
.index
"
)
&&
!
this
.
_is_empty_li
(
li
);
}
}
/**
/**
*
Adds header elements to
the used
and available
list
s
.
*
Delete all non-header items from
the
"
used
"
list.
*
*
* @method
* @method
* @p
aram {Object} headers - Object containing header text for used and available lists.
* @p
rivate
* @returns {void}
* @returns {void}
*/
*/
add_headers
(
headers
,
available_header
)
{
_delete_all_from_used
()
{
for
(
const
[
i
,
value
]
of
headers
.
entries
())
{
const
lis
=
document
.
querySelectorAll
(
'
.usedList li[data-id]
'
);
var
parentEl
=
document
.
getElementById
(
`usedList_
${
i
}
`
);
lis
.
forEach
(
li
=>
{
if
(
this
.
_deletable_li
(
li
))
{
this
.
_delete_li
(
li
);}});
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
);
}
}
/**
* Deletes a list item (li) from its parent node.
*
* @param {HTMLElement} li - The list item (li) element to delete.
*/
_delete_li
(
li
)
{
li
.
parentNode
.
removeChild
(
li
);
}
/**
/**
*
Updates the state based on changes in the used and available lists
.
*
Display a warning message on the question page
.
*
*
* @method
* @method
* @p
aram {Object} newUsed - Updated used list.
* @p
rivate
* @param {
Object} newAvailable - Updated available list
.
* @param {
string} msg - The message to be displayed in the warning
.
* @returns {void}
* @returns {void}
*/
*/
update_state
(
newUsed
,
newAvailable
)
{
_display_warning
(
msg
)
{
var
newState
=
{
used
:
newUsed
.
map
((
usedList
)
=>
usedList
.
map
((
used
)
=>
used
.
toArray
())),
available
:
newAvailable
.
toArray
()};
var
warning
=
document
.
createElement
(
"
div
"
);
if
(
this
.
inputId
!==
null
)
{
warning
.
className
=
"
sortable-warning
"
;
this
.
input
.
value
=
JSON
.
stringify
(
newState
);
var
exclamation
=
document
.
createElement
(
"
i
"
);
this
.
input
.
dispatchEvent
(
new
Event
(
'
change
'
));
exclamation
.
className
=
"
icon fa fa-exclamation-circle text-danger fa-fw
"
;
}
warning
.
append
(
exclamation
);
this
.
state
=
newState
;
var
warningMessage
=
document
.
createElement
(
"
span
"
);
warningMessage
.
textContent
=
msg
;
warning
.
append
(
warningMessage
);
document
.
body
.
insertBefore
(
warning
,
document
.
getElementById
(
"
sortableContainer
"
));
}
}
/**
/**
* Adds double-click listeners to move items upon double-click and updates the state accordingly.
* 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).
*
* This private method is called on items inside the used or available list.
*
*
* @method
* @method
* @p
aram {Object} newUsed - Updated used list.
* @p
rivate
* @param {
Object} newAvailable - Updated available list
.
* @param {
HTMLElement} item - The HTML element to check for double-clickability
.
* @returns {
void}
* @returns {
boolean} - Returns true if the element is double-clickable, false otherwise.
*/
*/
add_dblclick_listeners
(
newUsed
,
newAvailable
)
{
_double_clickable
(
item
)
{
this
.
available
.
addEventListener
(
'
dblclick
'
,
(
e
)
=>
{
return
!
item
.
matches
(
"
.header
"
);
if
(
this
.
_double_clickable
(
e
.
target
))
{
// get highest-level parent
var
li
=
this
.
_get_moveable_parent_li
(
e
.
target
);
li
=
(
this
.
clone
===
"
true
"
)
?
li
.
cloneNode
(
true
)
:
this
.
available
.
removeChild
(
li
);
this
.
used
[
0
].
append
(
li
);
this
.
update_state
(
newUsed
,
newAvailable
);
}
}
});
this
.
used
[
0
].
addEventListener
(
'
dblclick
'
,
(
e
)
=>
{
/* TODO : simplify this */
if
(
this
.
_double_clickable
(
e
.
target
))
{
_flip_orientation
()
{
// get highest-level parent
var
addClass
=
(
this
.
orientation
===
"
row
"
)
?
[
"
list-group
"
,
"
col
"
]
:
[
"
row
"
];
var
li
=
this
.
_get_moveable_parent_li
(
e
.
target
);
if
(
this
.
grid
)
{
this
.
used
[
0
].
removeChild
(
li
);
var
removeClass
=
(
this
.
orientation
===
"
row
"
)
?
[
"
list-group
"
,
"
row
"
]
:
[
"
list-group
"
,
"
col
"
];
if
(
this
.
clone
!==
"
true
"
)
{
var
currGridClass
=
(
this
.
orientation
===
"
row
"
)
?
"
grid-item-rigid
"
:
"
grid-item
"
;
this
.
available
.
insertBefore
(
li
,
this
.
available
.
children
[
1
]);
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
"
);
}
}
this
.
update_state
(
newUsed
,
newAvailable
);
})
}
}
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
)
{
* Add a click event listener to a button to delete all items from the "used" list and
var
indexDiv
=
document
.
getElementById
(
"
index
"
);
* updates the state accordingly.
indexDiv
.
classList
.
remove
(...
removeClass
);
*
indexDiv
.
classList
.
add
(...
addClass
);
* @method
if
(
this
.
orientation
===
"
col
"
)
{
* @param {string} buttonId - ID of the button element to attach the listener.
document
.
querySelectorAll
(
"
#index > .index
"
).
forEach
((
idx
)
=>
{
* @param {Object} newUsed - Updated "used" list.
if
(
!
idx
.
classList
.
contains
(
"
header
"
))
{
* @param {Object} newAvailable - Updated "available" list.
idx
.
classList
.
remove
(
"
index
"
);
* @returns {void}
idx
.
classList
.
add
(
"
header
"
);
*/
}
add_delete_all_listener
(
buttonId
,
newUsed
,
newAvailable
)
{
})
const
button
=
document
.
getElementById
(
buttonId
);
}
else
{
button
.
addEventListener
(
'
click
'
,
()
=>
{
document
.
querySelectorAll
(
'
#index > .header
'
).
forEach
((
header
)
=>
{
this
.
_delete_all_from_used
();
this
.
update_state
(
newUsed
,
newAvailable
);});
if
(
!
header
.
classList
.
contains
(
"
index
"
))
{
header
.
classList
.
remove
(
"
header
"
);
header
.
classList
.
add
(
"
index
"
);
}
})
}
}
this
.
orientation
=
(
this
.
orientation
===
"
row
"
)
?
"
col
"
:
"
row
"
;
}
}
/**
/**
* Generates the initial state of used and available items based on the provided proof steps and input ID.
* 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
* mode, `(n, 1, ?)` if `n` columns are specified and `(n, m, 1)` if `n` columns and `m` rows are specified.
*
*
* @method
* @method
* @private
* @private
* @param {Object}
proofS
teps - Object containing
proof
steps.
* @param {Object}
s
teps - Object containing steps.
* @param {string} inputId - ID of the input element for storing state.
* @param {string} inputId - ID of the input element for storing state.
* @returns {Object} The initial state object with used and available lists.
* @returns {Object} The initial state object with used and available lists.
*/
*/
_generate_state
(
proofS
teps
,
inputId
,
columns
,
rows
)
{
_generate_state
(
s
teps
,
inputId
,
columns
,
rows
)
{
const
usedState
=
(
rows
===
0
||
columns
===
0
)
?
const
usedState
=
(
rows
===
0
||
columns
===
0
)
?
Array
(
columns
).
fill
().
map
(()
=>
[[]])
:
Array
(
columns
).
fill
().
map
(()
=>
[[]])
:
Array
(
columns
).
fill
().
map
(()
=>
Array
(
rows
).
fill
([]));
Array
(
columns
).
fill
().
map
(()
=>
Array
(
rows
).
fill
([]));
let
stateStore
=
document
.
getElementById
(
inputId
);
let
stateStore
=
document
.
getElementById
(
inputId
);
if
(
stateStore
===
null
)
{
if
(
stateStore
===
null
)
{
return
{
used
:
usedState
,
available
:
[...
Object
.
keys
(
proofS
teps
)]};
return
{
used
:
usedState
,
available
:
[...
Object
.
keys
(
s
teps
)]};
}
}
return
(
stateStore
.
value
&&
stateStore
.
value
!=
""
)
?
return
(
stateStore
.
value
&&
stateStore
.
value
!=
""
)
?
JSON
.
parse
(
stateStore
.
value
)
:
JSON
.
parse
(
stateStore
.
value
)
:
{
used
:
usedState
,
available
:
[...
Object
.
keys
(
proofS
teps
)]};
{
used
:
usedState
,
available
:
[...
Object
.
keys
(
s
teps
)]};
}
}
/**
/**
* Validate if a given option key is among the possible option keys.
* Get the nearest moveable parent list item for a given HTML element.
*
* This private method traverses the DOM hierarchy starting from the provided HTML
* element and finds the nearest parent list item with the class ".list-group-item".
* It is useful for identifying the moveable parent when doubling clicking on child
* elements (for example MathJax display elements) inside list items.
*
*
* @method
* @method
* @private
* @private
* @param {string} key - The option key to validate.
* @param {HTMLElement} target - The HTML element for which to find the moveable parent list item.
* @param {string[]} possibleOptionKeys - List of possible option keys.
* @returns {HTMLElement|null} - The nearest parent list item with class ".list-group-item", or null if not found.
* @returns {boolean} - Returns true if the option key is valid, false otherwise.
*/
*/
_validate_option_key
(
key
,
possibleOptionKeys
)
{
_get_moveable_parent_li
(
target
)
{
return
possibleOptionKeys
.
includes
(
key
);
var
li
=
target
;
while
(
!
li
.
matches
(
"
.list-group-item
"
))
{
li
=
li
.
parentNode
;
}
return
li
;
}
}
/**
/**
*
Set and merge user-provided options with default options
.
*
Checks if a list item (li) element is empty
.
*
*
* This private method sets user options for both "used" and "available" lists
* @param {HTMLElement} li - The list item (li) element to check.
* by merging the provided options with the default options. If no options are
* @returns {boolean} - True if the list item is empty, otherwise false.
* provided, it returns the default options.
*
* @method
* @private
* @param {Object|null} options - Custom options for sortable lists
* of form {used: UsedOptions, available: AvailableOptions} (optional).
* @returns {Object} - Merged user options for "used" and "available" lists.
*/
*/
_set_user_options
(
options
)
{
_is_empty_li
(
li
)
{
var
userOptions
;
return
li
.
textContent
.
trim
()
===
''
&&
li
.
children
.
length
===
0
;
if
(
options
===
null
)
{
userOptions
=
this
.
defaultOptions
;
}
else
{
userOptions
=
{
used
:
Object
.
assign
(
this
.
defaultOptions
.
used
,
options
.
used
),
available
:
Object
.
assign
(
this
.
defaultOptions
.
available
,
options
.
available
)};
}
return
userOptions
;
}
}
/**
/**
...
@@ -900,130 +985,41 @@ export const stack_sortable = class {
...
@@ -900,130 +985,41 @@ export const stack_sortable = class {
}
}
/**
/**
* Display a warning message on the question page.
* Set and merge user-provided options with default options.
*
* @method
* @private
* @param {string} msg - The message to be displayed in the warning.
* @returns {void}
*/
_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
"
));
}
/**
* Create an HTML list item element based on keys in `this.proofSteps`.
*
* @method
* @private
* @param {string} proofKey - The key associated with the proof content in 'proofSteps'.
* @returns {HTMLElement} - The created list item element.
*/
_create_li
(
proofKey
,
opts
)
{
let
li
=
document
.
createElement
(
"
li
"
);
li
.
innerHTML
=
this
.
proofSteps
[
proofKey
];
this
.
_apply_attrs
(
li
,
{...{
"
data-id
"
:
proofKey
},
...
opts
});
li
.
className
=
this
.
item_class
;
return
li
;
}
_apply_attrs
(
el
,
opts
)
{
for
(
const
[
key
,
value
]
of
Object
.
entries
(
opts
))
{
el
.
setAttribute
(
key
,
value
);
}
}
/**
* Creates a header element.
*
* @method
* @private
* @param {string} innerHTML - Inner HTML content of the header.
* @param {string} id - ID of the header element.
* @returns {HTMLElement} The created header element.
*/
_create_header
(
innerHTML
,
id
,
opts
)
{
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
},
...
opts
});
return
i
;
}
_create_index
(
innerHTML
,
id
,
opts
)
{
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
},
...
opts
});
return
i
;
}
/**
* Check if an HTML element is double-clickable (i.e., it is not a header element).
*
* This private method is called on items inside the used or available list.
*
* @method
* @private
* @param {HTMLElement} item - The HTML element to check for double-clickability.
* @returns {boolean} - Returns true if the element is double-clickable, false otherwise.
*/
_double_clickable
(
item
)
{
return
!
item
.
matches
(
"
.header
"
);
}
/**
* Get the nearest moveable parent list item for a given HTML element.
*
*
* This private method traverses the DOM hierarchy starting from the provided HTML
* This private method sets user options for both "used" and "available" lists
* element and finds the nearest parent list item with the class ".list-group-item".
* by merging the provided options with the default options. If no options are
* It is useful for identifying the moveable parent when doubling clicking on child
* provided, it returns the default options.
* elements (for example MathJax display elements) inside list items.
*
*
* @method
* @method
* @private
* @private
* @param {HTMLElement} target - The HTML element for which to find the moveable parent list item.
* @param {Object|null} options - Custom options for sortable lists
* @returns {HTMLElement|null} - The nearest parent list item with class ".list-group-item", or null if not found.
* of form {used: UsedOptions, available: AvailableOptions} (optional).
* @returns {Object} - Merged user options for "used" and "available" lists.
*/
*/
_get_moveable_parent_li
(
target
)
{
_set_user_options
(
options
)
{
var
li
=
target
;
var
userOptions
;
while
(
!
li
.
matches
(
"
.list-group-item
"
))
{
if
(
options
===
null
)
{
li
=
li
.
parentNode
;
userOptions
=
this
.
defaultOptions
;
}
}
else
{
return
li
;
userOptions
=
{
used
:
Object
.
assign
(
this
.
defaultOptions
.
used
,
options
.
used
),
available
:
Object
.
assign
(
this
.
defaultOptions
.
available
,
options
.
available
)};
}
}
return
userOptions
;
_deletable_li
(
li
)
{
return
!
li
.
matches
(
"
.header
"
)
&&
!
li
.
matches
(
"
.index
"
)
&&
!
_is_empty_li
(
li
);
}
}
_delete_li
(
li
)
{
li
.
parentNode
.
removeChild
(
li
);
}
/**
/**
*
Delete all non-header items from the "used" list
.
*
Validate if a given option key is among the possible option keys
.
*
*
* @method
* @method
* @private
* @private
* @returns {void}
* @param {string} key - The option key to validate.
* @param {string[]} possibleOptionKeys - List of possible option keys.
* @returns {boolean} - Returns true if the option key is valid, false otherwise.
*/
*/
_delete_all_from_used
()
{
_validate_option_key
(
key
,
possibleOptionKeys
)
{
const
lis
=
document
.
querySelectorAll
(
'
.usedList li[data-id]
'
);
return
possibleOptionKeys
.
includes
(
key
);
lis
.
forEach
(
li
=>
{
if
(
this
.
_deletable_li
(
li
))
{
this
.
_delete_li
(
li
);}});
}
}
};
};
export
default
{
stack_sortable
};
export
default
{
stack_sortable
};
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment