Commits

Roger Haase  committed 90eb2ea

add auto-scroll edit textarea after doubleclick; auto-scroll show page after edit

  • Participants
  • Parent commits 4a05406

Comments (0)

Files changed (13)

File MoinMoin/apps/frontend/views.py

     name = 'usersettings_options'
     mailto_author = Checkbox.using(label=L_('Publish my email (not my wiki homepage) in author info'))
     edit_on_doubleclick = Checkbox.using(label=L_('Open editor on double click'))
+    scroll_page_after_edit = Checkbox.using(label=L_('Scroll page after edit'))
     show_comments = Checkbox.using(label=L_('Show comment sections'))
     disabled = Checkbox.using(label=L_('Disable this account forever'))
     submit = Submit.using(default=L_('Save'))

File MoinMoin/config/default.py

         css_url=None,
         mailto_author=False,
         edit_on_doubleclick=True,
+        scroll_page_after_edit=True,
         show_comments=False,
         want_trivial=False,
         disabled=False,

File MoinMoin/constants/keys.py

 SESSION_TOKEN = "session_token"
 RECOVERPASS_KEY = "recoverpass_key"
 EDIT_ON_DOUBLECLICK = "edit_on_doubleclick"
+SCROLL_PAGE_AFTER_EDIT = "scroll_page_after_edit"
 SHOW_COMMENTS = "show_comments"
 MAILTO_AUTHOR = "mailto_author"
 CSS_URL = "css_url"

File MoinMoin/converter/_util.py

 
 from __future__ import absolute_import, division
 
+from flask import request
+from flask import g as flaskg
+from emeraldtree import ElementTree as ET
+
 from MoinMoin.config import uri_schemes
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.mime import Type
-from MoinMoin.util.tree import moin_page
+from MoinMoin.util.tree import html, moin_page
 
 def allowed_uri_scheme(uri):
     parsed = Iri(uri)
 
     Collected items can be pushed back into the iterator and further calls will
     return them.
+
+    Increments a counter tracking the current line number. This is used by _Stack to
+    add an attribute used by javascript to autoscroll the edit textarea.
     """
 
-    def __init__(self, parent):
+    def __init__(self, parent, startno=0):
         self.__finished = False
         self.__parent = iter(parent)
         self.__prepend = []
+        self.lineno = startno
 
     def __iter__(self):
         return self
         if self.__finished:
             raise StopIteration
 
+        self.lineno += 1
         if self.__prepend:
             return self.__prepend.pop(0)
 
 
     def push(self, item):
         self.__prepend.append(item)
+        self.lineno -= 1
 
 
 class _Stack(object):
             else:
                 self.name = None
 
-    def __init__(self, bottom=None):
+    def __init__(self, bottom=None, iter_content=None):
         self._list = []
         if bottom:
             self._list.append(self.Item(bottom))
+        self.iter_content = iter_content
+        self.last_lineno = 0
 
     def __len__(self):
         return len(self._list)
 
+    def add_lineno(self, elem):
+        """
+        Add a custom attribute (data-lineno=nn) that will be used by Javascript to scroll edit textarea.
+        """
+        if request.user_agent and flaskg.user.edit_on_doubleclick:
+            # this is not py.test and user has option to edit on doubleclick
+            # TODO: move the 2 lines above and 2 related import statements outside of the converters (needed for standalone converter)
+            if self.last_lineno != self.iter_content.lineno:
+                # avoid adding same lineno to parent and multiple children or grand-children
+                elem.attrib[html.data_lineno] = self.iter_content.lineno
+                self.last_lineno = self.iter_content.lineno
+
     def clear(self):
         del self._list[1:]
 
         return self._list[-1].elem
 
     def top_append(self, elem):
+        if isinstance(elem, ET.Node):
+            self.add_lineno(elem)
         self.top().append(elem)
 
     def top_append_ifnotempty(self, elem):

File MoinMoin/converter/creole_in.py

             stack.push(moin_page.blockcode())
             return
 
-        lines = _Iter(self.block_nowiki_lines(iter_content))
+        lines = _Iter(self.block_nowiki_lines(iter_content), startno=iter_content.lineno)
 
         match = self.nowiki_interpret_re.match(firstline)
 
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack(body)
+        stack = _Stack(body, iter_content=iter_content)
 
         # Please note that the iterator can be modified by other functions
         for line in iter_content:

File MoinMoin/converter/html_out.py

     def visit_moinpage_table_of_content(self, elem):
         level = int(elem.get(moin_page.outline_level, 6))
 
-        attrib = {html.class_: 'moin-table-of-contents'}
-        elem = html.div(attrib=attrib)
+        attribs = elem.attrib.copy()
+        attribs[html.class_] = 'moin-table-of-contents'
+        elem = html.div(attrib=attribs)
 
         self._special_stack[-1].add_toc(elem, level)
         return elem

File MoinMoin/converter/mediawiki_in.py

 
         element = moin_page.table_body()
         stack.push(element)
-        lines = _Iter(self.block_table_lines(iter_content))
+        lines = _Iter(self.block_table_lines(iter_content), startno=iter_content.lineno)
         element = moin_page.table_row()
         stack.push(element)
         preprocessor_status = []
             if list_definition:
                 element_label = moin_page.list_item_label()
                 stack.top_append(element_label)
-                new_stack = _Stack(element_label)
+                new_stack = _Stack(element_label, iter_content=iter_content)
                 # TODO: definition list doesn't work,
                 #       if definition of the term on the next line
                 splited_text = text.split(':')
             element_body.level, element_body.type = level, type
 
             stack.push(element_body)
-            new_stack = _Stack(element_body)
+            new_stack = _Stack(element_body, iter_content=iter_content)
         else:
             new_stack = stack
             level = 0
 
         is_list = list_begin
-        iter = _Iter(self.indent_iter(iter_content, text, level, is_list))
+        iter = _Iter(self.indent_iter(iter_content, text, level, is_list), startno=iter_content.lineno)
         for line in iter:
             match = self.block_re.match(line)
             it = iter
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack(body)
+        stack = _Stack(body, iter_content=iter_content)
 
         for line in iter_content:
             match = self.indent_re.match(line)

File MoinMoin/converter/moinwiki_in.py

 
         nowiki_marker_len = len(nowiki_marker)
 
-        lines = _Iter(self.block_nowiki_lines(iter_content, nowiki_marker_len))
+        lines = _Iter(self.block_nowiki_lines(iter_content, nowiki_marker_len), startno=iter_content.lineno)
 
         if nowiki_interpret:
             if nowiki_args:
             if list_definition_text:
                 element_label = moin_page.list_item_label()
                 stack.top_append(element_label)
-                new_stack = _Stack(element_label)
+                new_stack = _Stack(element_label, iter_content=iter_content)
 
                 self.parse_inline(list_definition_text, new_stack, self.inline_re)
 
             element_body.level, element_body.type = level, type
 
             stack.push(element_body)
-            new_stack = _Stack(element_body)
+            new_stack = _Stack(element_body, iter_content=iter_content)
         else:
             new_stack = stack
 
-        iter = _Iter(self.indent_iter(iter_content, text, level))
+        iter = _Iter(self.indent_iter(iter_content, text, level), startno=iter_content.lineno)
         for line in iter:
             match = self.block_re.match(line)
             it = iter
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack(body)
+        stack = _Stack(body, iter_content=iter_content)
 
         for line in iter_content:
             data = dict(((str(k), v) for k, v in self.indent_re.match(line).groupdict().iteritems() if v is not None))

File MoinMoin/script/migration/moin19/import19.py

         bool_defaults = [ # taken from cfg.checkbox_defaults
             ('show_comments', 'False'),
             ('edit_on_doubleclick', 'True'),
+            ('scroll_page_after_edit', 'True'),
             ('want_trivial', 'False'),
             ('mailto_author', 'False'),
             ('disabled', 'False'),

File MoinMoin/storage/middleware/validation.py

     Boolean.named('want_trivial').using(optional=True),
     Boolean.named('show_comments').using(optional=True),
     Boolean.named('edit_on_doubleclick').using(optional=True),
+    Boolean.named('scroll_page_after_edit').using(optional=True),
     Boolean.named('mailto_author').using(optional=True),
     List.named('quicklinks').of(String.named('quicklinks')).using(optional=True),
     List.named('subscribed_items').of(String.named('subscribed_item')).using(optional=True),

File MoinMoin/templates/common.js

 /*jslint browser: true, */
 /*global $:false */
 
+// This file is a Jinja2 template and is not jslint friendly in its raw state.
+// To run jslint, use your browser debugging tools to view, copy and paste this file to jslint.
 
-// Enter edit mode when user doubleclicks within the content area.  Executed once on page load.
-function editOnDoubleClick() {
+
+// Utility function to add a message to moin flash area.
+var MOINFLASHHINT = "moin-flash moin-flash-hint",
+    MOINFLASHINFO = "moin-flash moin-flash-info",
+    MOINFLASHWARNING = "moin-flash moin-flash-warning",
+    MOINFLASHERROR = "moin-flash moin-flash-error";
+function moinFlashMessage(classes, message) {
     "use strict";
-    var modifyButton;
-    // is edit on doubleclick available for this user and item?
-    if (document.getElementById('moin-edit-on-doubleclick')) {
-        modifyButton = $('.moin-modify-button')[0];
-        if (modifyButton) {
-            // add a doubleclick action to the moin content
-            $('#moin-content').dblclick(function () {
-                document.location = modifyButton.href;
-            });
-        }
-    }
+    var pTag = '<P class="' + classes + '">' + message + '</p>';
+    $(pTag).appendTo($('#moin-flash'));
 }
-$(document).ready(editOnDoubleClick);
+
 
 // Highlight currently selected link in side panel. Executed on page load
 function selected_link() {
     $('#moin-usersettings form').submit(submitHandler);
 }
 $(document).ready(initMoinUsersettings);
+
+
+// This anonymous function supports doubleclick to edit, auto-scroll the edit textarea and page after edit
+$(function () {
+    // NOTE: if browser does not support sessionStorage, then auto-scroll is not available
+    //       sessionStorage is supported by FF3.5+, Chrome4+, Safari4+, Opera10.5+, and IE8+
+    "use strict";
+
+    var TOPID = 'moin-content',
+        LINENOATTR = "data-lineno",
+        MESSAGEMISSED = ' {{ _("You missed! Double-click on text or to the right of text to auto-scroll text editor.") }} ',
+        MESSAGEOBSOLETE = ' {{ _("Your browser is obsolete. Upgrade to gain auto-scroll text editor feature.") }} ',
+        OPERA = 'Opera', // special handling required because textareas have \r\n line endings
+        modifyButton,
+        lineno,
+        message,
+        caretLineno;
+
+    // called after +modify page loads -- scrolls the textarea after a doubleclick
+    function scrollTextarea(jumpLine) {
+        // jumpLine is textarea scroll-to line
+        var textArea = document.getElementById('f_content_form_data_text'),
+            textLines,
+            scrolledText,
+            scrollAmount,
+            textAreaClone;
+
+        if (textArea && textArea.setSelectionRange) {
+            window.scrollTo(0, 0);
+            // get data from textarea, split into array of lines, truncate based on jumpLine, make into a string
+            textLines = $(textArea).val();
+            scrolledText = textLines.split("\n"); // all browsers yield \n rather than \r\n or \r
+            scrolledText = scrolledText.slice(0, jumpLine);
+            if (navigator.userAgent && navigator.userAgent.substring(0, OPERA.length) === OPERA) {
+                scrolledText = scrolledText.join('\r\n') + '\r\n';
+            } else {
+                scrolledText = scrolledText.join('\n') + '\n';
+            }
+            // clone textarea, paste in truncated textArea data, measure height, delete clone
+            textAreaClone = $(textArea).clone(true);
+            textAreaClone = textAreaClone[0];
+            textAreaClone.id = "moin-textAreaClone";
+            textArea.parentNode.appendChild(textAreaClone);
+            $("#moin-textAreaClone").val(scrolledText);
+            textAreaClone.rows = 1;
+            scrollAmount = textAreaClone.scrollHeight - 100; // get total height of clone - 100 pixels
+            textAreaClone.parentNode.removeChild(textAreaClone);
+            // position the caret, highlight the position of the caret for a second or so
+            textArea.focus();
+            if (scrollAmount > 0) { textArea.scrollTop = scrollAmount; }
+            textArea.setSelectionRange(scrolledText.length, scrolledText.length + 8);
+            setTimeout(function () {textArea.setSelectionRange(scrolledText.length, scrolledText.length + 4); }, 1000);
+            setTimeout(function () {textArea.setSelectionRange(scrolledText.length, scrolledText.length); }, 1500);
+        }
+    }
+
+    // called after a "show" page loads, scroll page to textarea caret position
+    function scrollPage(lineno) {
+        var elem = document.getElementById(TOPID),
+            notFound = true,
+            RADIX = 10,
+            saveColor;
+
+        lineno = parseInt(lineno, RADIX);
+        // find a starting point at bottom of moin-content
+        while (elem.lastChild) { elem = elem.lastChild; }
+        // walk DOM backward looking for a lineno attr equal or less than lineno
+        while (notFound && elem.id !== TOPID) {
+            if (elem.hasAttribute && elem.hasAttribute(LINENOATTR) && parseInt(elem.getAttribute(LINENOATTR), RADIX) <= lineno) {
+                notFound = false;
+            }
+            if (notFound) {
+                if (elem.previousSibling) {
+                    elem = elem.previousSibling;
+                    while (elem.lastChild) { elem = elem.lastChild; }
+                } else {
+                    elem = elem.parentNode;
+                }
+            }
+        }
+        // scroll element into view and then back off 100 pixels
+        // TODO: does not scroll when user setting for show comments is off; user toggles show comments on; user doubleclicks and updates comments; (elem has display:none)
+        elem.scrollIntoView();
+        window.scrollTo(window.pageXOffset, window.pageYOffset - 100);
+        // highlight background of selected element for a second or so
+        saveColor = elem.style.backgroundColor;
+        elem.style.backgroundColor = 'yellow';
+        setTimeout(function () { elem.style.backgroundColor = saveColor; }, 1500);
+    }
+
+    // called after user doubleclicks, return a line number close to doubleclick point
+    function findLineNo(elem) {
+        var lineno;
+        // first try easy way via jquery checking event node and all parent nodes
+        lineno = $(elem).closest("[" + LINENOATTR + "]");
+        if (lineno.length) { return $(lineno).attr(LINENOATTR); }
+        // walk DOM backward looking for a lineno attr among siblings, cousins, uncles...
+        while (elem.id !== TOPID) {
+            if (elem.hasAttribute && elem.hasAttribute(LINENOATTR)) {
+                // not perfect, a lineno prior to target
+                return elem.getAttribute(LINENOATTR);
+            }
+            if (elem.previousSibling) {
+                elem = elem.previousSibling;
+                while (elem.lastChild) { elem = elem.lastChild; }
+            } else {
+                elem = elem.parentNode;
+            }
+        }
+        // user double-clicked on dead zone so we walked back to #moin-content
+        return 0;
+    }
+
+    // called after user clicks OK button to save edit changes
+    function getCaretLineno(textArea) {
+        // return the line number of the textarea caret
+        var caretPoint,
+            textLines;
+        if (textArea.selectionStart) {
+            caretPoint = textArea.selectionStart;
+        } else {
+            // IE9 - user has clicked ouside of textarea and textarea focus and caret position has been lost
+            return 0;
+        }
+        // get textarea text, split at caret, return number of lines before caret
+        if (navigator.userAgent && navigator.userAgent.substring(0, OPERA.length) === OPERA) {
+            textLines = textArea.value;
+        } else {
+            textLines = $(textArea).val();
+        }
+        textLines = textLines.substring(0, caretPoint);
+        return textLines.split("\n").length;
+    }
+
+    // doubleclick processing starts here
+    if (Storage !== "undefined") {
+        // Start of processing for "show" pages
+        if (document.getElementById('moin-edit-on-doubleclick')) {
+            // this is a "show" page and the edit on doubleclick option is set for this user
+            modifyButton = $('.moin-modify-button')[0];
+            if (modifyButton) {
+                // add doubleclick event handler when user doubleclicks within the content area
+                $('#moin-content').dblclick(function (e) {
+                    // get clicked line number, save, and go to +modify page
+                    lineno = findLineNo(e.target);
+                    sessionStorage.moinDoubleLineNo = lineno;
+                    document.location = modifyButton.href;
+                });
+            }
+            if (sessionStorage.moinCaretLineNo) {
+                // we have just edited this page; scroll "show" page to last position of caret in edit textarea
+                scrollPage(sessionStorage.moinCaretLineNo);
+                sessionStorage.removeItem('moinCaretLineNo');
+            }
+        }
+
+        // Start of processing for "modify" pages
+        if (sessionStorage.moinDoubleLineNo) {
+            // this is a +modify page, scroll the textarea to the doubleclicked line
+            lineno = sessionStorage.moinDoubleLineNo;
+            sessionStorage.removeItem('moinDoubleLineNo');
+            if (lineno === '0') {
+                // give user a hint because the double-click was a miss
+                moinFlashMessage(MOINFLASHINFO, MESSAGEMISSED);
+                lineno = 1;
+            }
+            scrollTextarea(lineno - 1);
+            // is option to scroll page after edit set?
+            if (document.getElementById('moin-scroll-page-after-edit')) {
+                // add click handler to OK (save) button to capture position of caret in textarea
+                $("#f_submit").click(function () {
+                    caretLineno = getCaretLineno(document.getElementById('f_content_form_data_text'));
+                    // save lineno for use in "show" page load
+                    sessionStorage.moinCaretLineNo = caretLineno;
+                });
+            }
+        }
+    } else {
+        // provide reduced functionality for obsolete browsers that do not support local storage: IE6, IE7, etc.
+        if (document.getElementById('moin-edit-on-doubleclick')) {
+            moinFlashMessage(MOINFLASHWARNING, MESSAGEOBSOLETE);
+            modifyButton = $('.moin-modify-button')[0];
+            if (modifyButton) {
+                // add doubleclick event handler when user doubleclicks within the content area
+                $('#moin-content').dblclick(function (e) {
+                    document.location = modifyButton.href;
+                });
+            }
+        }
+    }
+});

File MoinMoin/templates/modify.html

     {{ gen.form.close() }}
 </div>
 {% endblock %}
+
+{% block options_for_javascript %}
+{%- if user.edit_on_doubleclick -%}
+    <br id="moin-scroll-page-after-edit" />
+{%- endif %}
+{% endblock %}

File MoinMoin/templates/usersettings_forms.html

 <dl>
     {{ forms.render(form['mailto_author']) }}
     {{ forms.render(form['edit_on_doubleclick']) }}
+    {{ forms.render(form['scroll_page_after_edit']) }}
     {{ forms.render(form['show_comments']) }}
     {{ forms.render(form['disabled']) }}
 </dl>