File:  [LON-CAPA] / modules / damieng / graphical_editor / loncapa_daxe / web / nodes / chem.dart
Revision 1.4: download - view: text, annotated - select for diffs
Thu Dec 15 20:52:49 2016 UTC (7 years, 5 months ago) by damieng
Branches: MAIN
CVS tags: HEAD
fixed undo/redo/drag&drop for chem display

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

/**
 * Display for the chem element.
 * Jaxe display type: 'chem'.
 */
class Chem extends DNString {
  bool preview;
  StreamSubscription<h.MouseEvent> listener = null;
  
  Chem.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
    preview = false;
  }
  
  Chem.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
    if (firstChild is DNText && firstChild.nextSibling == null)
      firstChild.replaceWith(new ChemText(firstChild.nodeValue));
    preview = previewIsPossible();
  }
  
  bool previewIsPossible() {
    return (firstChild is DNText && !firstChild.nodeValue.trim().startsWith('\$'));
  }
  
  @override
  h.Element html() {
    if (!preview) {
      if (listener == null)
        listener = h.document.onClick.listen((h.MouseEvent e) => onClick(e));
      return super.html();
    }
    h.SpanElement span = new h.SpanElement();
    span.id = "$id";
    span.classes.add('dn');
    if (!valid)
      span.classes.add('invalid');
    String text = firstChild.nodeValue.trim();
    span.setInnerHtml(Chem.textToHTML(text));
    span.onClick.listen((h.MouseEvent e) {
      preview = false;
      updateHTML();
      // prevent calling onClick(e):
      // (it could switch back to preview mode if the element takes more
      // space and moves to the next line after switching to non-preview).
      e.stopPropagation();
      e.preventDefault();
      // but set cursor inside:
      page.moveCursorTo(new Position(this, this.offsetLength));
    });
    setupDrag(span);
    return(span);
  }
  
  void onClick(h.MouseEvent e) {
    if (!previewIsPossible())
      return; // NOTE: that will generate a lot of events for nothing when variables are used...
    h.Element node = getHTMLNode();
    if (node == null) {
      listener.cancel();
      listener = null;
      return;
    }
    h.Rectangle r = node.getBoundingClientRect(); // could be more precise with getClientRects
    if (!r.containsPoint(e.client)) {
      listener.cancel();
      listener = null;
      preview = true;
      updateHTML();
      e.stopPropagation();
      e.preventDefault();
      page.cursor.refresh();
    }
  }
  
  @override
  void updateHTML() {
    if (preview && !previewIsPossible())
      preview = false;
    super.updateHTML();
  }
  
  @override
  void updateHTMLAfterChildrenChange(List<DaxeNode> changed) {
    if (preview)
      updateHTML();
    else
      super.updateHTMLAfterChildrenChange(changed);
  }
  
  @override
  h.Element getHTMLContentsNode() {
    if (!preview)
      return super.getHTMLContentsNode();
    return(getHTMLNode());
  }

  @override
  Position firstCursorPositionInside() {
    if (!preview)
      return super.firstCursorPositionInside();
    return(null);
  }
  
  @override
  Position lastCursorPositionInside() {
    if (!preview)
      return super.lastCursorPositionInside();
    return(null);
  }
  
  @override
  bool get needsSpecialDNText {
    return true;
  }
  
  @override
  DNText specialDNTextConstructor(String text) {
    return new ChemText(text);
  }
  
  static String textToHTML(String reaction) {
    // this is doing the same thing as chemparse, except it uses UNICODE characters instead of LaTeX
    //List<String> tokens = reaction.split(new RegExp(r"(\s\+|\->|<=>|<\-|\.)"));
    // this did not work (delimiters are not preserved)
    // using look-ahead/behind does not work either...
    List<String> tokens = getTokensWithDelimiters(reaction, new RegExp(r"(\s\+|\->|<=>|<\-|\.)"));
    String formula = '';
    for (int i=0; i<tokens.length; i++) {
        String token = tokens[i];
        if (token == '->' ) {
            formula += '&#8594; ';
            continue;
        }
        if (token == '<-' ) {
            formula += '&#8592; ';
            continue;
        }  
        if (token == '<=>') {
            formula += '&#8652; ';
            continue;
        }
        if (token == '.') {
          formula = formula.replaceFirst(new RegExp(r"&nbsp;| "), '');
          formula += '&middot;';
          continue;
        }
        Iterable<Match> matches = new RegExp(r"^\s*([\d|\/]*(?:&frac\d\d)?)(.*)").allMatches(token);
        String molecule;
        if (matches.length > 0) {
          Match firstMatch = matches.first;
          if (firstMatch.group(1) != null)
            formula += firstMatch.group(1); // stoichiometric coefficient
          if (firstMatch.group(2) != null)
            molecule = firstMatch.group(2);
          else
            molecule = '';
        } else
          molecule = '';
        // subscripts
        // $molecule =~ s|(?<=[a-zA-Z\)\]\s])(\d+)|<sub>$1</sub>|g;
        // Javascript does not support look-behind like Perl
        molecule = molecule.replaceAllMapped(new RegExp(r"([a-zA-Z\)\]\s])(\d+)"), (Match m) => "${m[1]}<sub>${m[2]}</sub>");
        // superscripts
        molecule = molecule.replaceAllMapped(new RegExp(r"\^(\d*[+\-]*)"), (Match m) => "<sup>${m[1]}</sup>");
        // strip whitespace
        molecule = molecule.replaceAll(new RegExp(r"\s*"), '');
        // forced space
        molecule = molecule.replaceAll('_', ' ');
        molecule = molecule.replaceAll('-', '&minus;');
        formula += molecule + '&nbsp;';
    }
    // get rid of trailing space
    formula = formula.replaceFirst(new RegExp(r"(&nbsp;| )$"), '');
    return formula;
  }
  
  static List<String> getTokensWithDelimiters(String s, Pattern p) {
    int start = 0;
    int ind = s.indexOf(p, start);
    List<String> list = new List<String>();
    while (ind >= 0) {
      if (ind > 0)
        list.add(s.substring(start, ind));
      Match m = p.matchAsPrefix(s, ind);
      String delimiter = m.group(0);
      list.add(delimiter);
      start = ind + delimiter.length;
      ind = s.indexOf(p, start);
    }
    if (start < s.length)
      list.add(s.substring(start));
    return list;
  }
}

/**
 * A DNText which triggers an update for the parent Chem when preview is true.
 * This is needed to handle undo of a part of the text when in preview mode.
 */
class ChemText extends DNText {
  
  ChemText(String s) : super(s) {
  }
  
  ChemText.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
  }
  
  @override
  void updateHTML() {
    if (parent is Chem && (parent as Chem).preview)
      parent.updateHTML();
    else
      super.updateHTML();
  }
}

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>