Commit a077490d authored by Bas Lijnse's avatar Bas Lijnse

Refactored selection components to always use explicit identification

parent cfb68c34
itasks.Dropdown = {
domTag: 'select',
width: 'wrap',
initDOMEl: function() {
var me = this,
el = me.domEl,
value = me.value[0],
option;
option = document.createElement('option');
option.innerHTML = "Select...";
option.value = -1;
el.appendChild(option);
me.options.forEach(function(label,index) {
option = document.createElement('option');
option.value = index;
option.innerHTML = label;
if(index === value) {
option.selected = true;
}
el.appendChild(option);
},me);
el.addEventListener('change',function(e) {
var value = e.target.value | 0;
me.doEditEvent(me.taskId,me.editorId,value == -1 ? null : value,false);
});
},
setValue: function(selection) {
var me = this,
value;
if(selection.length == 0) {
value = -1;
} else {
value = selection[0];
}
me.domEl.value = value;
}
};
itasks.ListSelect = { //Mixin for selections in lists based on index
//Mixin containing the selection/toggle behavior
itasks.Selector = {
select: function (selection, toggle = false) {
var me = this,
options = me.optionsEl.children,
numOptions = options.length,
options = me.options,
oldSelection = me.value.slice(0),
i;
if(toggle) {
//Unselect items in the toggle set
me.value = me.value.filter(function(x) {return !selection.includes(x)});
......@@ -56,10 +14,18 @@ itasks.ListSelect = { //Mixin for selections in lists based on index
me.value = selection;
}
//Update DOM
for(i = 0; i < numOptions; i++) {
me.selectInDOM(options[i],me.value.includes(i));
options.forEach(me.selectOptionsInDOM.bind(me));
},
selectOptionsInDOM: function(option) {
var me = this;
me.selectInDOM(option.domEl,me.value.includes(option.id));
if(option.children) {
option.children.forEach(me.selectOptionsInDOM.bind(me));
}
},
selectInDOM(el,selected) {
el.classList[selected ? 'add':'remove'](this.cssPrefix + 'selected');
},
onAttributeChange: function(name,value) {
if(name == 'value') {
me.select(value,false);
......@@ -67,6 +33,44 @@ itasks.ListSelect = { //Mixin for selections in lists based on index
}
};
itasks.Dropdown = Object.assign({
domTag: 'select',
width: 'wrap',
multiple: false,
initDOMEl: function() {
var me = this,
el = me.domEl,
optionEl;
//The empty selection
optionEl = document.createElement('option');
optionEl.innerHTML = "Select...";
optionEl.value = "";
el.appendChild(optionEl);
me.options.forEach(function(option) {
optionEl = document.createElement('option');
optionEl.value = option.id;
optionEl.innerHTML = option.text;
if(me.value.includes(option.id)) {
optionEl.selected = true;
}
el.appendChild(optionEl);
option.domEl = optionEl;
},me);
el.addEventListener('change',function(e) {
me.select(e.target.value === '' ? [] : [parseInt(e.target.value)]);
me.doEditEvent(me.taskId,me.editorId,me.value);
});
},
selectInDOM: function(el,selected) {
el.selected = selected;
}
},itasks.Selector);
itasks.CheckGroup = Object.assign({
domTag: 'ul',
cssCls: 'checkgroup',
......@@ -74,8 +78,7 @@ itasks.CheckGroup = Object.assign({
initDOMEl: function() {
var me = this,
el = me.domEl,
inputName = "choice-" + me.taskId + "-" + me.editorId,
value = me.value.length ? me.value[0] : null;
inputName = "choice-" + me.taskId + "-" + me.editorId;
me.options.forEach(function(option,idx) {
var liEl,inputEl,labelEl;
......@@ -84,61 +87,59 @@ itasks.CheckGroup = Object.assign({
inputEl.type = me.multiple ? 'checkbox' : 'radio';
inputEl.value = idx;
inputEl.name = inputName;
inputEl.id = inputName + "-option-" + idx;
if(idx === value) {
inputEl.id = inputName + "-option-" + option.id;
if(me.value.includes(option.id)) {
inputEl.checked = true;
}
inputEl.addEventListener('click',function(e) {
me.select([idx],me.multiple);
me.select([option.id],me.multiple);
me.doEditEvent(me.taskId,me.editorId,me.value);
});
liEl.appendChild(inputEl);
labelEl = document.createElement('label');
labelEl.setAttribute('for',inputName + "-option-" + idx);
labelEl.innerHTML = option;
labelEl.setAttribute('for',inputName + "-option-" + option.id);
labelEl.innerHTML = option.text;
liEl.appendChild(labelEl);
el.appendChild(liEl);
option.domEl = liEl;
});
me.optionsEl = me.domEl;
me.optionsDOM = me.domEl.children;
},
selectInDOM: function(el,selected) {
el.children[0].checked = selected;
}
},itasks.ListSelect);
},itasks.Selector);
itasks.ChoiceList = Object.assign({
cssCls: 'choice-list',
multiple: false,
initDOMEl: function() {
var me = this,
el = me.domEl,
value = me.value.length ? me.value[0] : null;
el = me.domEl;
me.options.forEach(function(option,idx) {
var optionEl;
optionEl = document.createElement('div');
optionEl.classList.add(me.cssPrefix + 'choice-list-option');
if(idx === value) {
optionEl.classList.add(me.cssPrefix + 'selected');
}
optionEl.addEventListener('click',function(e) {
me.select([idx], me.multiple && (e.metaKey || e.ctrlKey));
me.select([option.id], me.multiple && (e.metaKey || e.ctrlKey));
me.doEditEvent(me.taskId,me.editorId,me.value);
e.preventDefault();
});
optionEl.innerHTML = option;
optionEl.innerHTML = option.text;
el.appendChild(optionEl);
option.domEl = optionEl;
});
me.optionsEl = me.domEl;
me.optionsDOM = me.domEl.children;
},
selectInDOM(el,selected) {
el.classList[selected ? 'add':'remove'](this.cssPrefix + 'selected');
}
},itasks.ListSelect);
},itasks.Selector);
itasks.Grid = Object.assign({
cssCls: 'choicegrid',
......@@ -167,12 +168,12 @@ itasks.Grid = Object.assign({
me.options.forEach(function(option,rowIdx) {
rowEl = document.createElement('div');
rowEl.addEventListener('click',function(e) {
me.select([rowIdx], me.multiple && (e.metaKey || e.ctrlKey));
me.select([option.id], me.multiple && (e.metaKey || e.ctrlKey));
me.doEditEvent(me.taskId,me.editorId,me.value);
},me);
if(me.doubleClickAction) {
rowEl.addEventListener('dblclick',function(e) {
me.select([rowIdx]);
me.select([option.id]);
me.doEditEvent(me.taskId,me.editorId,me.value);
me.sendActionEvent(me.doubleClickAction[0],me.doubleClickAction[1]);
......@@ -180,31 +181,32 @@ itasks.Grid = Object.assign({
e.preventDefault();
},me);
}
option.forEach(function(cell) {
option.cells.forEach(function(cell) {
cellEl = document.createElement('div');
cellEl.innerHTML = cell;
rowEl.appendChild(cellEl);
});
bodyEl.appendChild(rowEl);
option.domEl = rowEl;
});
//Indicate selection
//Indicate initial selection
if(me.value.length) {
me.value.forEach(function(selectedIdx) {
bodyEl.childNodes[selectedIdx].classList.add(me.cssPrefix + 'selected');
});
}
el.appendChild(bodyEl);
me.optionsEl = me.bodyEl;
},
initContainerEl: function() {},
selectInDOM(el,selected) {
el.classList[selected ? 'add':'remove'](this.cssPrefix + 'selected');
}
},itasks.ListSelect);
},itasks.Selector);
itasks.Tree = {
itasks.Tree = Object.assign({
height: 'flex',
multiple: false,
initDOMEl: function() {
var me = this,
el = me.domEl,
......@@ -215,15 +217,11 @@ itasks.Tree = {
rootNode = document.createElement('ol');
//Create a table for quick access
me.selection = me.value || [];
me.nodes = [];
me.options.forEach(function(option,idx) {
me.addNode(option,rootNode,rootNodeId,idx);
},me);
me.selection.forEach(function(idx) {
me.value.forEach(function(idx) {
me.nodes[idx].classList.add(me.cssPrefix + 'selected');
});
el.appendChild(rootNode);
......@@ -235,27 +233,25 @@ itasks.Tree = {
nodeId = rootNodeId + "-"+ idx;
node = document.createElement('li');
node.id = nodeId;
if(option.leaf) {
node.classList.add(me.cssPrefix + 'leaf');
}
label = document.createElement('label');
label.id = nodeId + "-l";
if(option.iconCls) {
label.classList.add(option.iconCls);
} else {
label.classList.add(me.cssPrefix + 'default-' + (option.leaf ? 'leaf' : 'folder'));
label.classList.add(me.cssPrefix + 'default-' + (option.children.length ? 'folder' : 'leaf'));
}
label.innerHTML = option.text;
label.addEventListener('click',function(e) {
me.setValue([option.value]);
me.doEditEvent(me.taskId,me.editorId,["sel",option.value,true]);
me.select([option.id],me.multiple && (e.metaKey || e.ctrlKey));
me.doEditEvent(me.taskId,me.editorId,me.value);
},me);
if(me.doubleClickAction) {
label.addEventListener('dblclick',function(e) {
me.setValue([option.value]);
me.doEditEvent(me.taskId,me.editorId,["sel",option.value,true]);
me.select([option.id]);
me.doEditEvent(me.taskId,me.editorId,me.value);
me.doEditEvent(me.doubleClickAction[0],null,me.doubleClickAction[1]);
e.stopPropagation();
......@@ -273,7 +269,7 @@ itasks.Tree = {
childExpand.checked = true;
}
childExpand.addEventListener('click',function(e) {
me.doEditEvent(me.taskId,me.editorId,["exp",option.value,childExpand.checked]);
//me.doEditEvent(me.taskId,me.editorId,["exp",option.value,childExpand.checked]);
},me);
node.appendChild(childExpand);
......@@ -284,19 +280,8 @@ itasks.Tree = {
node.appendChild(childOl);
}
parentNode.appendChild(node);
//Keep reference
me.nodes[option.value] = node;
},
setValue: function(value) {
var me = this;
me.selection.forEach(function(idx) {
me.nodes[idx].classList.remove(me.cssPrefix + 'selected');
});
me.selection = value;
me.selection.forEach(function(idx) {
me.nodes[idx].classList.add(me.cssPrefix + 'selected');
});
//Track the option in the dom
option.domEl = node;
}
};
},itasks.Selector);
......@@ -4,9 +4,7 @@ import iTasks.API.Core.Tasks
from iTasks.API.Core.Types import :: Date, :: Time, :: DateTime, :: Action
from Data.Functor import class Functor
from iTasks.UI.Editor.Builtin import :: ChoiceNode, :: ChoiceGrid
//from iTasks.API.Core.Types import :: ChoiceTree, :: ChoiceTreeValue, :: Date, :: Time, :: DateTime, :: Action
//import Data.Functor
from iTasks.UI.Editor.Builtin import :: ChoiceText, :: ChoiceGrid, :: ChoiceNode
/*** General input/update/output tasks ***/
......@@ -26,9 +24,9 @@ from iTasks.UI.Editor.Builtin import :: ChoiceNode, :: ChoiceGrid
| E.v: UpdateSharedAs (a -> v) (a v -> b) (v v -> v) & iTask v
//Selection in arbitrary containers (explicit identification is needed)
:: SelectOption c s = SelectInDropdown (c -> [String]) (c [Int] -> [s])
| SelectInCheckGroup (c -> [String]) (c [Int] -> [s])
| SelectInList (c -> [String]) (c [Int] -> [s])
:: SelectOption c s = SelectInDropdown (c -> [ChoiceText]) (c [Int] -> [s])
| SelectInCheckGroup (c -> [ChoiceText]) (c [Int] -> [s])
| SelectInList (c -> [ChoiceText]) (c [Int] -> [s])
| SelectInGrid (c -> ChoiceGrid) (c [Int] -> [s])
| SelectInTree (c -> [ChoiceNode]) (c [Int] -> [s])
......
......@@ -15,7 +15,7 @@ import iTasks._Framework.Tonic
import iTasks.UI.Layout, iTasks.UI.Definition, iTasks.UI.Editor, iTasks.UI.Prompt, iTasks.UI.Editor.Builtin
import Text.HTML
derive class iTask ChoiceGrid, ChoiceNode
derive class iTask ChoiceText, ChoiceGrid, ChoiceRow, ChoiceNode
enterInformation :: !d ![EnterOption m] -> Task m | toPrompt d & iTask m
enterInformation d [EnterAs fromf:_]
......@@ -255,14 +255,14 @@ editSharedMultipleChoiceWithSharedAs d vopts sharedContainer target sharedSel
//Helper functions for the edit*Choice* tasks
selectOption target opts = case opts of
[(ChooseFromDropdown f):_] = SelectInDropdown (toLabels f) (findSelection target)
[(ChooseFromCheckGroup f):_] = SelectInCheckGroup (toLabels f) (findSelection target)
[(ChooseFromList f):_] = SelectInList (toLabels f) (findSelection target)
[(ChooseFromDropdown f):_] = SelectInDropdown (toTexts f) (findSelection target)
[(ChooseFromCheckGroup f):_] = SelectInCheckGroup (toTexts f) (findSelection target)
[(ChooseFromList f):_] = SelectInList (toTexts f) (findSelection target)
[(ChooseFromGrid f):_] = SelectInGrid (toGrid f) (findSelection target)
_ = SelectInDropdown (toLabels id) (findSelection target)
_ = SelectInDropdown (toTexts id) (findSelection target)
toLabels f options = map (toSingleLineText o f) options
toGrid f options = {ChoiceGrid|header=gText{|*|} AsHeader (fixtype vals),rows = [map Text (gText{|*|} AsRow (Just v)) \\ v <- vals]}
toTexts f options = [{ChoiceText|id=i,text=toSingleLineText (f o)} \\ o <- options & i <- [0..]]
toGrid f options = {ChoiceGrid|header=gText{|*|} AsHeader (fixtype vals),rows = [{ChoiceRow|id=i,cells=map Text (gText{|*|} AsRow (Just v))} \\ v <- vals & i <- [0..]]}
where
vals = map f options
......
......@@ -585,8 +585,8 @@ derive gEditor ButtonState
gText{|Table|} _ _ = ["<Table>"]
gEditor{|Table|} = liftEditor toGrid fromGrid (grid (multipleAttr False))
where
toGrid (Table header rows mbSel) = ({ChoiceGrid|header=header,rows=rows},maybeToList mbSel)
fromGrid ({ChoiceGrid|header,rows},sel) = Table header rows (listToMaybe sel)
toGrid (Table header rows mbSel) = ({ChoiceGrid|header=header,rows=[{ChoiceRow|id=i,cells=cells} \\ cells <- rows & i <- [0..]]},maybeToList mbSel)
fromGrid ({ChoiceGrid|header,rows},sel) = Table header [cells \\ {ChoiceRow|cells} <- rows] (listToMaybe sel)
gDefault{|Table|} = Table [] [] Nothing
......
......@@ -34,16 +34,24 @@ progressBar :: UIAttributes -> Editor (Maybe Int,Maybe String) //Percentage, d
// ## Selection components ##
// UIDropdown, UIRadioGroup, UICheckboxGroup, UIChoiceList, UIGrid, UITree
dropdown :: UIAttributes -> Editor ([String], [Int])
checkGroup :: UIAttributes -> Editor ([String], [Int])
choiceList :: UIAttributes -> Editor ([String], [Int])
dropdown :: UIAttributes -> Editor ([ChoiceText], [Int])
checkGroup :: UIAttributes -> Editor ([ChoiceText], [Int])
choiceList :: UIAttributes -> Editor ([ChoiceText], [Int])
grid :: UIAttributes -> Editor (ChoiceGrid, [Int])
tree :: UIAttributes -> Editor ([ChoiceNode], [Int])
//Convenient types for describing the values of grids and trees
:: ChoiceText =
{ id :: Int
, text :: String
}
:: ChoiceGrid =
{ header :: [String]
, rows :: [[HtmlTag]]
, rows :: [ChoiceRow]
}
:: ChoiceRow =
{ id :: Int
, cells :: [HtmlTag]
}
:: ChoiceNode =
......
......@@ -49,19 +49,23 @@ progressBar attr = viewComponent combine UIProgressBar
where
combine (amount,text) = 'DM'.unions ((maybe [] (\t -> [textAttr t]) text) ++ (maybe [] (\v -> [valueAttr (JSONInt v)]) amount) ++ [attr])
dropdown :: UIAttributes -> Editor ([String], [Int])
dropdown attr = choiceComponent (const attr) id JSONString (\o i -> i >= 0 && i < length o) UIDropdown
dropdown :: UIAttributes -> Editor ([ChoiceText], [Int])
dropdown attr = choiceComponent (const attr) id toOptionText checkBoundsText UIDropdown
checkGroup :: UIAttributes -> Editor ([String],[Int])
checkGroup attr = choiceComponent (const attr) id JSONString (\o i -> i >= 0 && i < length o) UICheckGroup
checkGroup :: UIAttributes -> Editor ([ChoiceText], [Int])
checkGroup attr = choiceComponent (const attr) id toOptionText checkBoundsText UICheckGroup
choiceList :: UIAttributes -> Editor ([String],[Int])
choiceList attr = choiceComponent (const attr) id JSONString (\o i -> i >= 0 && i < length o) UIChoiceList
choiceList :: UIAttributes -> Editor ([ChoiceText], [Int])
choiceList attr = choiceComponent (const attr) id toOptionText checkBoundsText UIChoiceList
toOptionText {ChoiceText|id,text}= JSONObject [("id",JSONInt id),("text",JSONString text)]
checkBoundsText options idx = or [id == idx \\ {ChoiceText|id} <- options]
grid :: UIAttributes -> Editor (ChoiceGrid, [Int])
grid attr = choiceComponent (\{ChoiceGrid|header} -> 'DM'.union attr (columnsAttr header)) (\{ChoiceGrid|rows} -> rows) toOption (\o i -> i >= 0 && i < length o) UIGrid
grid attr = choiceComponent (\{ChoiceGrid|header} -> 'DM'.union attr (columnsAttr header)) (\{ChoiceGrid|rows} -> rows) toOption checkBounds UIGrid
where
toOption opt = JSONArray (map (JSONString o toString) opt)
toOption {ChoiceRow|id,cells}= JSONObject [("id",JSONInt id),("cells",JSONArray (map (JSONString o toString) cells))]
checkBounds options idx = or [id == idx \\ {ChoiceRow|id} <- options]
tree :: UIAttributes -> Editor ([ChoiceNode], [Int])
tree attr = choiceComponent (const attr) id toOption checkBounds UITree
......@@ -69,9 +73,8 @@ where
toOption {ChoiceNode|id,label,icon,expanded,children}
= JSONObject [("text",JSONString label)
,("iconCls",maybe JSONNull (\i -> JSONString ("icon-"+++i)) icon)
,("value",JSONInt id)
,("id",JSONInt id)
,("expanded",JSONBool expanded)
,("leaf",JSONBool (isEmpty children))
,("children",JSONArray (map toOption children))
]
......@@ -130,17 +133,12 @@ where
= case e of
JSONNull
= (Ok (NoChange,FieldMask {touched=True,valid=optional,state=JSONNull}),(val,[]),vst)
(JSONArray indices)
# selection = [i \\ JSONInt i <- indices]
(JSONArray ids)
# selection = [i \\ JSONInt i <- ids]
| all (checkBounds options) selection
= (Ok (NoChange,FieldMask {touched=True,valid=True,state=JSONArray indices}),(val,selection),vst)
| otherwise
= (Error ("Choice event out of bounds: " +++ toString (JSONArray indices)),(val,sel),vst)
(JSONInt idx)
| checkBounds options idx
= (Ok (NoChange,FieldMask {touched=True,valid=True,state=JSONInt idx}),(val,[idx]),vst)
= (Ok (NoChange,FieldMask {touched=True,valid=True,state=JSONArray ids}),(val,selection),vst)
| otherwise
= (Error ("Choice event out of bounds: " +++ toString idx),(val,sel),vst)
= (Error ("Choice event out of bounds: " +++ toString (JSONArray ids)),(val,sel),vst)
_
= (Error ("Invalid choice event: " +++ toString e), (val,sel),vst)
......
......@@ -112,7 +112,8 @@ where
= (viz,{vst & selectedConsIndex = selectedConsIndex})
| otherwise
//Initially only generate a UI to choose a constructor
# consChooseUI = uia UIDropdown (choiceAttrs taskId (editorId dp) [] [JSONString gdc.gcd_name \\ gdc <- gtd_conses])
# consOptions = [JSONObject [("id",JSONInt i),("text",JSONString gdc.gcd_name)] \\ gdc <- gtd_conses & i <- [0..]]
# consChooseUI = uia UIDropdown (choiceAttrs taskId (editorId dp) [] consOptions)
# consChooseMask = FieldMask {touched=False,valid=optional,state=JSONNull}
= (Ok (UI UIVarCons 'DM'.newMap [consChooseUI],CompoundMask [consChooseMask]),{vst & selectedConsIndex = selectedConsIndex})
Update
......@@ -122,7 +123,8 @@ where
| otherwise
= case ex.Editor.genUI dp x vst of
(Ok (UI UICons attr items, CompoundMask masks),vst)
# consChooseUI = uia UIDropdown (choiceAttrs taskId (editorId dp) [vst.selectedConsIndex] [JSONString gdc.gcd_name \\ gdc <- gtd_conses])
# consOptions = [JSONObject [("id",JSONInt i),("text",JSONString gdc.gcd_name)] \\ gdc <- gtd_conses & i <- [0..]]
# consChooseUI = uia UIDropdown (choiceAttrs taskId (editorId dp) [vst.selectedConsIndex] consOptions)
# consChooseMask = FieldMask {touched=False,valid=True,state=JSONInt vst.selectedConsIndex}
= (Ok (UI UIVarCons attr [consChooseUI:items],CompoundMask [consChooseMask:masks]),{vst & selectedConsIndex = selectedConsIndex})
(Error e,vst) = (Error e,vst)
......@@ -145,7 +147,7 @@ where
# consChooseMask = FieldMask {touched=True,valid=optional,state=JSONNull}
= (Ok (change,CompoundMask [consChooseMask:masks]),OBJECT val, vst)
onEdit dp ([],JSONInt consIdx) (OBJECT val) (CompoundMask [FieldMask {FieldMask|touched,valid,state}:masks]) vst=:{VSt|mode} //Update is a constructor switch
onEdit dp ([],JSONArray [JSONInt consIdx]) (OBJECT val) (CompoundMask [FieldMask {FieldMask|touched,valid,state}:masks]) vst=:{VSt|mode} //Update is a constructor switch
| consIdx < 0 || consIdx >= gtd_num_conses
= (Error "Constructor selection out of bounds",OBJECT val,vst)
//Create a default value for the selected constructor
......
......@@ -5,7 +5,7 @@ import iTasks.UI.Editor.Builtin, iTasks.UI.Definition
import qualified Data.Map as DM
import Text.HTML
derive class iTask ChoiceGrid, ChoiceNode
derive class iTask ChoiceText, ChoiceGrid, ChoiceRow, ChoiceNode
testBuiltinEditors :: TestSuite
testBuiltinEditors = testsuite "Builtin editors" "These tests check if the builtin editors work"
......@@ -89,23 +89,25 @@ where
testDropdown = itest "Dropdown" "Check if the dropdown works" "You should be able to edit" tut
where
tut :: Task ([String],[Int])
tut = testEditor (dropdown (multipleAttr False)) (["A","B","C"],[]) Update
tut :: Task ([ChoiceText],[Int])
tut = testEditor (dropdown (multipleAttr False)) ([{ChoiceText|id=0,text="A"},{ChoiceText|id=1,text="B"},{ChoiceText|id=2,text="C"}],[]) Update
testCheckGroup = itest "Check group" "Check if the checkgroup works" "You should be able to edit" tut
where
tut :: Task ([String],[Int])
tut = testEditor (checkGroup (multipleAttr False)) (["A","B","C"],[]) Update
tut :: Task ([ChoiceText],[Int])
tut = testEditor (checkGroup (multipleAttr False