File:  [LON-CAPA] / modules / damieng / graphical_editor / loncapa_daxe / web / nodes / option_foilgroup.dart
Revision 1.11: download - view: text, annotated - select for diffs
Wed Mar 8 20:31:22 2017 UTC (7 years, 3 months ago) by damieng
Branches: MAIN
CVS tags: HEAD
making sure that getHTMLContentsNode() returns something for foilgroups

/*
  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>