/*
This file is part of LONCAPA-Daxe.
LONCAPA-Daxe is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
LONCAPA-Daxe is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Daxe. If not, see <http://www.gnu.org/licenses/>.
*/
part of loncapa_daxe;
/**
* This is used by OptionResponse for simple UI.
*/
class OptionFoilgroup extends LCDBlock {
static List<String> simpleUIAttributes = ['options','checkboxvalue','checkboxoptions'];
x.Element foilRef;
List<String> options;
bool checkboxes;
String checkedValue, uncheckedValue;
String simpleFoilNamePrefix = 'foil'; // can be changed to 'Foil'
OptionFoilgroup.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
initOptionFoilgroup();
}
OptionFoilgroup.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
initOptionFoilgroup();
simpleUI = parent is OptionResponse && parent.simpleUI && simpleUIPossibleNoThrow();
if (simpleUI)
setupSimpleUI();
}
void initOptionFoilgroup() {
List<x.Element> foilRefs = doc.cfg.elementReferences('foil');
foilRef = doc.cfg.findSubElement(ref, foilRefs);
updateOptionsFromAttributes();
}
void updateOptionsFromAttributes() {
options = parseOptions();
checkedValue = getAttribute('checkboxvalue');
if (checkedValue == '')
checkedValue = null;
checkboxes = (checkedValue != null && options.length == 2 && options.indexOf(checkedValue) >= 0);
if (checkboxes) {
if (options.indexOf(checkedValue) == 0)
uncheckedValue = options[1];
else
uncheckedValue = options[0];
}
}
@override
bool simpleUIPossible() {
for (DaxeAttr att in attributes) {
if (!simpleUIAttributes.contains(att.localName)) {
throw new SimpleUIException('foilgroup: ' + LCDStrings.get('attribute_problem') + att.name);
}
}
for (DaxeNode dn=firstChild; dn!= null; dn=dn.nextSibling) {
if (dn is OptionFoil) {
if (!dn.simpleUIPossible())
return false;
} else if (dn.nodeType != DaxeNode.TEXT_NODE) {
String title = doc.cfg.elementTitle(dn.ref);
throw new SimpleUIException(LCDStrings.get('element_prevents') + ' ' + dn.nodeName +
(title != null ? " ($title)" : ''));
} else if (dn.nodeValue.trim() != '') {
return false;
}
}
return true;
}
void setupRestrictions() {
if (restrictedInserts == null)
restrictedInserts = ['foil'];
}
/**
* Returns the OptionFoil children
*/
List<OptionFoil> getFoils() {
List<OptionFoil> list = new List<OptionFoil>();
for (DaxeNode dn in childNodes) {
if (dn is OptionFoil)
list.add(dn);
}
return list;
}
@override
void setupSimpleUI() {
simpleUI = true;
setupRestrictions();
// Removes the name attributes, remove unused foils, add a foil if there is none
if (getFoils().length == 0) {
addFoil();
} else {
for (OptionFoil foil in getFoils()) {
if (foil.getAttribute('name') != null) {
if (foil.getAttribute('name').startsWith('Foil'))
simpleFoilNamePrefix = 'Foil';
foil.removeAttribute('name');
}
if (foil.getAttribute('value') == 'unused')
removeChild(foil);
}
}
options = parseOptions();
}
@override
h.Element html() {
simpleUI = parent is OptionResponse && (parent as OptionResponse).simpleUI;
if (!simpleUI)
return super.html();
setupRestrictions();
options = parseOptions();
h.DivElement div = new h.DivElement();
div.id = id;
div.classes.add('option-foilgroup');
if (!checkboxes)
div.append(optionsHtml());
div.append(foilsHtml());
return div;
}
/**
* Returns the HTML for editing the list of options.
*/
h.Element optionsHtml() {
h.DivElement div = new h.DivElement();
div.id = "options$id";
div.classes.add('optionresponse-options');
div.appendText(LCDStrings.get('possible_options'));
h.UListElement ul = new h.UListElement();
ul.classes.add('option-list');
int startIndex = -1;
for (int i=0; i<options.length; i++) {
String value = options[i];
h.LIElement li = new h.LIElement();
li.draggable = true;
li.onDragStart.listen((h.MouseEvent e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text', "option$i");
startIndex = i; // see http://stackoverflow.com/q/11065803/438970
});
li.onDragEnd.listen((h.MouseEvent e) {
startIndex = -1;
});
li.onDragOver.listen((h.MouseEvent e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
int enterLeaveCount = 0; // see http://stackoverflow.com/q/7110353/438970
li.onDragEnter.listen((h.MouseEvent e) {
if (startIndex == -1)
return;
e.preventDefault(); // for IE11 (without this there are more dragenter than dragleave)
enterLeaveCount++;
if (startIndex < i)
li.classes.add('dragafter');
else if (i < startIndex)
li.classes.add('dragbefore');
});
li.onDragLeave.listen((h.MouseEvent e) {
if (startIndex == -1)
return;
e.preventDefault();
enterLeaveCount--;
if (enterLeaveCount == 0) {
li.classes.remove('dragafter');
li.classes.remove('dragbefore');
}
});
li.onDrop.listen((h.MouseEvent e) => drop(i, e));
h.ImageElement grip = new h.ImageElement(src:'images/grip.png', width:8, height:16);
grip.classes.add('grip-icon');
grip.draggable = false;
li.append(grip);
h.TextInputElement textfield = new h.TextInputElement();
textfield.size = 30;
textfield.value = value;
textfield.onMouseEnter.listen((h.MouseEvent e) {
// IE bug workaround
// https://connect.microsoft.com/IE/feedback/details/927470/ie-11-input-field-of-type-text-does-not-respond-to-mouse-clicks-when-ancestor-node-has-draggable-true
if (startIndex == -1)
li.draggable = false;
});
textfield.onMouseLeave.listen((h.MouseEvent e) {
if (!li.draggable)
li.draggable = true;
});
textfield.onInput.listen((h.Event e) {
UndoableEdit compound = new UndoableEdit.compound(LCDStrings.get('options_change'));
for (DaxeNode dn=firstChild; dn!= null; dn=dn.nextSibling) {
if (dn is OptionFoil && dn.getAttribute('value') == options[i])
compound.addSubEdit(new UndoableEdit.changeAttribute(dn,
new DaxeAttr('value', textfield.value), updateDisplay:false));
}
options[i] = textfield.value;
compound.addSubEdit(saveOptionsEdit(updateDisplay:false));
doc.doNewEdit(compound);
updateFoilsHtml();
});
// NOTE: it is not possible to select text in the textfield
// see http://stackoverflow.com/q/21680363/438970
li.append(textfield);
h.ImageElement deleteImg = new h.ImageElement(src:'images/delete.png', width:16, height:16);
deleteImg.classes.add('delete-icon');
deleteImg.onClick.listen((h.MouseEvent e) => deleteOption(i));
li.append(deleteImg);
ul.append(li);
}
div.append(ul);
h.ButtonElement bAdd = new h.ButtonElement();
bAdd.attributes['type'] = 'button';
bAdd.text = LCDStrings.get('add_option');
bAdd.onClick.listen((h.MouseEvent event) => addOption());
div.append(bAdd);
return div;
}
/**
* Returns the HTML for editing the foils.
*/
h.Element foilsHtml() {
h.DivElement div = new h.DivElement();
div.id = "foils$id";
div.classes.add('optionresponse-foilgroup');
div.appendText(LCDStrings.get('foils'));
h.TableElement table = new h.TableElement();
table.id = "contents-$id";
for (DaxeNode dn=firstChild; dn!= null; dn=dn.nextSibling) {
table.append(dn.html());
}
div.append(table);
h.ButtonElement bAdd = new h.ButtonElement();
bAdd.attributes['type'] = 'button';
bAdd.text = LCDStrings.get('add_foil');
bAdd.onClick.listen((h.MouseEvent event) => addUndoableFoil());
div.append(bAdd);
return div;
}
void addOption() {
options.add('');
optionsChange();
}
void deleteOption(int index) {
options.removeAt(index);
optionsChange();
}
/**
* Handle a change to the options
*/
void optionsChange() {
UndoableEdit compound = new UndoableEdit.compound(LCDStrings.get('options_change'));
UndoableEdit optionEdit = saveOptionsEdit();
compound.addSubEdit(optionEdit);
for (DaxeNode dn in childNodes) {
if (dn is OptionFoil) {
String value = dn.getAttribute('value');
if (value == null && options.length > 0)
compound.addSubEdit(new UndoableEdit.changeAttribute(dn, new DaxeAttr('value', options[0])));
else if (options.indexOf(value) == -1) {
// current foil value is no longer in the list: set to first option or remove
String newValue;
if (options.length > 0)
newValue = options[0];
else
newValue = null;
compound.addSubEdit(new UndoableEdit.changeAttribute(dn, new DaxeAttr('value', newValue)));
}
}
}
doc.doNewEdit(compound);
}
/* not called
void updateOptionsHtml() {
h.document.getElementById("options$id").replaceWith(optionsHtml());
}
*/
void updateFoilsHtml() {
h.document.getElementById("foils$id").replaceWith(foilsHtml());
}
void drop(int targetIndex, h.MouseEvent e) {
String data = e.dataTransfer.getData('text');
e.preventDefault();
e.stopPropagation();
if (!data.startsWith('option'))
return;
int startIndex = int.parse(data.substring(6));
String value = options[startIndex];
if (targetIndex > startIndex) {
for (int i=startIndex; i<targetIndex; i++)
options[i] = options[i+1];
options[targetIndex] = value;
} else if (targetIndex < startIndex) {
for (int i=startIndex; i>targetIndex; i--)
options[i] = options[i-1];
options[targetIndex] = value;
}
optionsChange();
}
@override
void updateAttributes() {
if (!simpleUI) {
super.updateAttributes();
return;
}
updateOptionsFromAttributes();
updateHTML();
}
/**
* Add a foil (not using an UndoableEdit)
*/
void addFoil() {
OptionFoil foil = new OptionFoil.fromRef(foilRef);
if (options.length > 0) {
if (checkboxes)
foil.setAttribute('value', uncheckedValue);
else
foil.setAttribute('value', options[0]);
}
foil.state = 1;
appendChild(foil);
}
/**
* Add a foil (using an UndoableEdit)
*/
void addUndoableFoil() {
OptionFoil foil = new OptionFoil.fromRef(foilRef);
if (options.length > 0) {
if (checkboxes)
foil.setAttribute('value', uncheckedValue);
else
foil.setAttribute('value', options[0]);
}
foil.state = 1;
doc.insertNode(foil, new Position(this, offsetLength));
page.moveCursorTo(new Position(foil, 0));
page.updateAfterPathChange();
}
/**
* Removes the given foil (called when the user click on the button).
*/
void removeFoil(OptionFoil foil) {
doc.doNewEdit(new UndoableEdit.removeNode(foil));
}
/**
* Returns the list of options, parsed from the attributes, or an empty list if there is a parsing error.
*/
List<String> parseOptions() {
String s = getAttribute('options');
if (s == null)
return new List<String>();
// NOTE: if you really want a regexp, take a look at http://stackoverflow.com/q/8493195/438970
// I think this code is clearer
List<String> list = new List<String>();
bool inList, inString, needComma, backslash;
String option, stringDelimiter;
inList = false;
for (int i=0; i<s.length; i++) {
String c = s[i];
if (inList) {
if (inString) {
if (c == stringDelimiter && !backslash) {
inString = false;
needComma = true;
list.add(option);
} else if (c == '\\' && !backslash) {
backslash = true;
} else {
option += c;
if (backslash)
backslash = false;
}
} else {
if (c == ')')
inList = false;
else if (!needComma && (c == "'" || c == '"')) {
inString = true;
backslash = false;
option = '';
stringDelimiter = c;
} else if (c == ',' && needComma) {
needComma = false;
} else if (c != ' ')
return [];
}
} else {
if (c == '(') {
inList = true;
inString = false;
needComma = false;
} else if (c != ' ')
return [];
}
}
if (inList)
return [];
return list;
}
UndoableEdit saveOptionsEdit({bool updateDisplay:true}) {
StringBuffer sb = new StringBuffer();
sb.write('(');
for (int i=0; i<options.length; i++) {
String option = options[i];
option = option.replaceAll("'", "\\'");
sb.write("'$option'");
if (i < options.length - 1)
sb.write(',');
}
sb.write(')');
UndoableEdit edit = new UndoableEdit.changeAttribute(this,
new DaxeAttr('options', sb.toString()), updateDisplay:updateDisplay);
// FIXME: consecutive character additions should be merged into one Undo
return edit;
}
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>