jspwiki-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From brus...@apache.org
Subject svn commit: r1686927 [6/11] - in /jspwiki/trunk: ./ jspwiki-war/src/main/config/wro/ jspwiki-war/src/main/java/org/apache/wiki/ jspwiki-war/src/main/resources/templates/ jspwiki-war/src/main/scripts/behaviors/ jspwiki-war/src/main/scripts/dialog/ jspwi...
Date Mon, 22 Jun 2015 20:11:44 GMT
Modified: jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Snipe.js
URL: http://svn.apache.org/viewvc/jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Snipe.js?rev=1686927&r1=1686926&r2=1686927&view=diff
==============================================================================
--- jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Snipe.js (original)
+++ jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Snipe.js Mon Jun 22 20:11:42 2015
@@ -1,4 +1,4 @@
-/*!
+/*
     JSPWiki - a JSP-based WikiWiki clone.
 
     Licensed to the Apache Software Foundation (ASF) under one
@@ -29,7 +29,7 @@ Class: Snipe
 
 Credit:
     Snipe (short for Snip-Editor) was inspired by postEditor (by Daniel Mota aka IceBeat,
-    http://icebeat.bitacoras.com ) and ''textMate'' (http://macromates.com/).
+    http://icebeat.bitacoras.com ) and ""textMate"" (http://macromates.com/).
     It has been written to fit as wiki markup editor for the JSPWIKI project.
 
 Arguments:
@@ -58,8 +58,8 @@ Options:
         dialogs suchs as Font, Color and Special.
         See property [initializeDialogs] and [openDialog]
     findForm - (object) list of form-controls. See [onFindAndReplace] handler.
-    next - (Element), when pressing Shift-Enter, the textarea will ''blur'' and
-        this ''next'' element will ge the focus.
+    next - (Element), when pressing Shift-Enter, the textarea will ""blur"" and
+        this ""next"" element will ge the focus.
         This compensates the overwritting default TAB handling of the browser.
     onresize - (function, optional), when present, a textarea resize bar
         with css class {{resize-bar}} is added after the textarea,
@@ -75,30 +75,30 @@ Dependencies:
 Example:
 (start code)
     new Snipe( "mainTextarea", {
-        snippets: { bold:"**{bold text}**", italic:"''{italic text}''" },
+        snippets: { bold:"**{bold text}**", italic:"""{italic text}""" },
         tabcompletion:true,
-        directsnips: { '(':')', '[' : ']' },
-        buttons: $$('a.tool'),
-        next:'nextInputField'
+        directsnips: { "(":")", "[" : "]" },
+        buttons: $$("a.tool"),
+        next:"nextInputField"
     });
 (end)
 
 */
 var Snipe = new Class({
 
-    Implements: [Options, Events],
+    Implements: [Options, Events, Undoable],
 
-    Binds: ['sync','shortcut','keystroke','suggest'],
+    Binds: ["sync","shortcut","keystroke","suggest","select","action"],
 
     options: {
         tab: "    ", //default tab = 4 spaces
         //autosuggest:false,
         //tabcompletion:false,
-        //autocompletion:false, 
+        //autocompletion:false,
         snippets: {},
-        directsnips: {}, 
+        directsnips: {},
         //container: null,   //DOM element, container for toolbar buttons
-        sectionCursor: 'all',
+        sectionCursor: "all",
         sectionParser: function(){ return {}; }
     },
 
@@ -106,133 +106,93 @@ var Snipe = new Class({
 
         options = this.setOptions(options).options;
 
+        this.initializeUndoable(options.undoBtns);
+
         var self = this,
 
             /*
-            The textarea is cloned into a mainarea and workarea.
-            The workarea is visible and used for the actual editing.
+            The textarea is cloned into a main and work textarea.
+            The work texarea is visible and used for the actual editing.
             It contains either the full document or a particular section.
-            The mainarea is hidden and contains always the full document.
+            The main textarea is hidden and is kept always up to date.
             On submit, the mainarea is send back to the server.
             */
             main = self.mainarea = $(el),
-            work = main.clone().erase('name') //.clone(true,false), dont copy ID and name
-                .inject( main.hide(), 'before' ),
-            container = options.container || work.form;
+            work = main.clone().erase("name").inject( main.hide(), "before" ),
+            container = options.container || work.form,
 
             // Augment the textarea element with extra capabilities
             // Make sure the content of the mainarea is always in sync with the workarea
             textarea = self.textarea = new Textarea( work );
 
-        self.undoredo = new UndoRedo( self, {
-            undo:container.getElement('[data-cmd=undo]'), 
-            redo:container.getElement('[data-cmd=redo]')
-        });
+        //console.log("**** JUST CREATED THE SNIPE WORK EDITAEREA!  Huray!! ****");
 
+        self.directsnips = new Snipe.Snips( textarea, options.directsnips );
+        self.snippets    = new Snipe.Snips( textarea, options.snippets );
 
-        //The Commands class processes all commands
-        //   entered via tab-completion, button clicks, dialogs or suggestion dialogs.
-        //   Valid commands are given back to the Snipe editor via the onAction event.
-        self.commands = new Snipe.Commands( container, {
-
-            onOpen: function(/*cmd, eventHdl*/){ /*work.focus();*/ },
-
-            onClose: function(){ work.focus(); },
-
-            onAction: function(cmd){ self.action(cmd, Array.slice(arguments,1) ); },
+        self.snippets.dialogs.find = [ Dialog.Find, {
+            data: {
+                //feed the find dialog with searchable content
+                get: function(){
+                    var selection = textarea.getSelection();
+                    return (selection=="") ? work.value : selection;
+                },
+                set: function(v){
+                    var s = textarea.getSelectionRange();
+                    self.fireEvent("beforeChange");
+                    s.thin ? work.value = v : textarea.setSelection(v);
+                }
+            }
+        }];
 
-            //predefined dialogs
-            dialogs: {
+        //console.log("snip dialogs",JSON.encode(Object.keys(self.snippets.dialogs)));
 
-                find: [ Dialog.Find, {
-
-                    //dialog: container.getElement('.dialog.find'),
-
-                    data: {
-                        //feed the find dialog with searchable content: selection or all
-                        get: function(){
-                            var s = textarea.getSelection();
-                            return (s=='') ? work.value : s;
-                        },
-                        set: function(v){
-                            var s = textarea.getSelectionRange();
-                            self.undoredo.onChange();
-                            s.thin ? work.value = v : textarea.setSelection(v);
-                        }
-                    }
+        //Snipe.Commands takes care of capturing commands.
+        //Commands are entered via tab-completion, button clicks, or a dialog.
+        //Snipe.Commands also ensures that at most one dialog is open at the same time.
+        self.commands = new Snipe.Commands( container, {
+            //onOpen: function( /*command*/ ){ work.focus(); },
+            onClose: function( /*command*/ ){ work.focus(); },
+            onAction: self.action,
 
-                }]
-            }
+            dialogs: self.snippets.dialogs,
+            relativeTo: textarea
         });
 
-        self.initSnippets( options.snippets );
-        self.clearContext();
 
+        self.reset();
+
+        //Activate snipe trigger events on the work textarea
         work.addEvents({
+
             keydown: self.keystroke,
             keypress: self.keystroke,
-            //fixme: any click outside the suggestion block should clear the active snip -- blur ?
-            //blur: self.clearContext.bind(self), //(and hide any open dialogs)
-            keyup: self.suggest,
-            click: self.suggest,
-            change: function(parm){ self.fireEvent('change',parm); }
-        });
-
-        //catch shortcut keys when focus on toolbar or textarea
-        container.addEvent('keypress', self.shortcut);
-
-    },
-
-    /*
-    Function: initSnippets
-        Initialize the snippets and collect all shortcut keys and suggestion snips
-    */
-    initSnippets: function( snips ){
-
-        var self = this, 
-            cmd, snip, key, dialogs = {},
-            ismac = Browser.Platform.mac, //navigator.userAgent.indexOf('Mac OS X')!=-1
-            shortcut = (ismac ? 'meta+' : 'control+');
-
-        self.keys = {};
-        self.suggestions = {};
-
-        for( cmd in snips ){
-        
-            snip = snips[cmd];
-
-            if( typeOf(snip)=='string' ){ snip = {snippet:snip}; }
-
-            Function.from( snip.initialize )(cmd, snip);
 
-            if( key = snip.key ){ 
-                if( key.indexOf('+')<0 ){ key = shortcut+key; }
-                self.keys[key.toLowerCase()] = cmd; 
+            //fixme: any click outside the suggestion block should clear the context -- blur ?
+            //blur: self.reset.bind(self), //(and hide any open dialogs; and update the preview area...)
+            keyup: self.suggest.debounce(), //(250, true)
+            click: self.suggest.debounce(),
+            //select: self.select.debounce(),
+            change: function( parm ){
+                //console.log("bubble change event from textarea");
+                self.fireEvent("change",parm);
             }
 
-            if( typeOf(snip.suggest)=='function' ){ self.suggestions[cmd] = snip; }
-
-            //check for default snip dialogs -- they have the same name as the command
-            //EG:  find:{find:[Dialog.Find,{options}] }
-            if( snip[cmd] ){ dialogs[cmd] = snip[cmd]; }
+        });
 
-        }
+        //catch shortcut keys when focus on toolbar and/or textarea
+        container.addEvent("keydown", self.shortcut);
 
-        //initialize all detected dialogs
-        console.log("snip dialogs",Object.keys(dialogs).length);
-        self.commands.addDialogs(dialogs, self.textarea);
-        
     },
 
-
     /*
     Function: toElement
         Retrieve textarea DOM element;
 
     Example:
-    >    var snipe = new Snipe('textarea-element');
-    >    $('textarea-element') == snipe.toElement();
-    >    $('textarea-element') == $(snipe);
+    >    var snipe = new Snipe("textarea-element");
+    >    $("textarea-element") == snipe.toElement();
+    >    $("textarea-element") == $(snipe);
 
     */
     toElement: function(){
@@ -241,22 +201,22 @@ var Snipe = new Class({
 
     /*
     Function: get
-        Retrieve some of the public properties of the snip-editor.
+        Retrieve some of the public properties or options of the snip-editor.
 
     Arguments:
         item - textarea|snippets|tabcompletion|directsnips|smartpairs|autosuggest
     */
     get: function(item){
 
-        return( /mainarea|textarea/.test(item) ? this[item] : 
-                    /snippets|directsnips|autosuggest|tabcompletion|smartpairs/.test(item) ? this.options[item] : 
-                        null );
+        return( /mainarea|textarea/.test(item) ? this[item] :
+                /snippets|directsnips|autosuggest|tabcompletion|smartpairs/.test(item) ? this.options[item] :
+                null );
 
     },
 
     /*
     Function: set
-        Set/Reset some of the public options of the snip-editor.
+        Set/Reset some of the options of the snip-editor.
 
     Arguments:
         item - snippets|tabcompletion|directsnips|smartpairs|autosuggest
@@ -274,28 +234,37 @@ var Snipe = new Class({
 
     /*
     Function: shortcut.
-        Handle shortcut keys: Ctrl+shortcut key.
+        Handle shortcut keys: Ctrl/Meta+shortcut key.
         This is a "Keypress" event handler connected to the container element
         of the snip editor.
     Note:
         Safari seems to choke on Cmd+b and Cmd+i. All other Cmd+keys are fine. !?
         It seems in those cases, the event is fired on document level only.
+
+        More info http://unixpapa.com/js/key.html
     */
     shortcut: function(e){
 
-        var key = (e.shift ? 'shift+':'') + 
-                    (e.control ? 'control+':'') + 
-                      (e.meta ? 'meta+':'') + 
-                        (e.alt ? 'alt+':'') + 
+        var key, keycmd;
+
+        if( e.shift || e.control || e.meta || e.alt ){
+
+            key = (e.shift ? "shift+":"") +
+                    (e.control ? "control+":"") +
+                      (e.meta ? "meta+":"") +
+                        (e.alt ? "alt+":"") +
                           e.key,
 
-            keycmd = this.keys[key];
+            //console.log("shortcut ",key);
+            keycmd = this.snippets.keys[key];
 
-        //console.log(key);
-        if ( keycmd ){
-            console.log(this.keys,'shortcut',key,keycmd);
-            e.stop();
-            this.commands.action( keycmd );
+            if ( keycmd ){
+
+                console.log("Snipe shortcut",key,keycmd,e.code);
+                e.stop();
+                this.commands.action( keycmd );
+
+            }
         }
 
     },
@@ -323,28 +292,28 @@ var Snipe = new Class({
 
         //console.log(e.key, e.code + " keystroke "+e.shift+" "+e.type+"+meta="+e.meta+" +ctrl="+e.control );
 
-        if( e.type=='keydown' ){
+        if( e.type=="keydown" ){
 
             //Exit if this is a normal key; process special chars with the keydown event
             if( e.key.length==1 ) return;
 
-        } else { // e.type=='keypress'
+        } else { // e.type=="keypress"
 
             //CHECKME
             //Only process regular character keys via keypress event
-            //Note: cross-browser hack with 'which' attribute for special chars
+            //Note: cross-browser hack with "which" attribute for special chars
             if( !e.event.which /*which==0*/ ){ return; }
 
-            //CHECKME: Reset faulty 'special char' treatment by mootools
+            //CHECKME: Reset faulty "special char" treatment by mootools
             //console.log( e.key, String.fromCharCode(e.code).toLowerCase());
-            
+
             e.key = String.fromCharCode(e.code).toLowerCase();
 
         }
 
         var self = this,
             txta = self.textarea,
-            el = $(txta),
+            el = txta.toElement(),
             key = e.key,
             caret = txta.getSelectionRange(),
             scroll = el.getScroll();
@@ -353,7 +322,7 @@ var Snipe = new Class({
 
         if( /up|down|esc/.test(key) ){
 
-            self.clearContext();
+            self.reset();
 
         } else if( /tab|enter|delete|backspace/.test(key) ){
 
@@ -361,7 +330,7 @@ var Snipe = new Class({
 
         } else {
 
-            self.directSnippet(e, txta, caret);
+            self.smartPairs(e, txta, caret);
 
         }
 
@@ -371,8 +340,8 @@ var Snipe = new Class({
 
     /*
     Function: enter
-        When the Enter key is pressed, the next line will be ''auto-indented''
-        or space-aligned with the previous line.
+        When the Enter key is pressed, the next line will be auto-indented
+        with the previous line.
         Except if the Enter was pressed on an empty line.
 
     Arguments:
@@ -382,27 +351,20 @@ var Snipe = new Class({
     */
     enter: function(e, txta, caret) {
 
-        //if( this.hasContext() ){
-            //fixme
-            //how to 'continue previous snippet ??
-            //eg '\n* {unordered list item}' followed by TAB or ENTER
-            //snippet should always start with \n;
-            //snippet should have a 'continue on enter' flag ?
-        //}
+        this.reset();
 
-        this.clearContext();
+        var prevline = txta.getFromStart().match( /(?:^|\r?\n)([ \t]+)(.*)$/ );
+        //prevline[1]=sequence of spaces (indentation string)
+        //prevline[2]=non-blank tail of the prevline
 
-        if( caret.thin ){
+        if( !e.shift && prevline && (prevline[2]!="") ){
 
-            var prevline = txta.getFromStart().split(/\r?\n/).pop(),
-                indent = prevline.match( /^\s+/ );
-
-            if( indent && (indent != prevline) ){
-                e.stop();
-                txta.insertAfter( '\n' + indent[0] );
-            }
+            e.stop();
+            //console.log("enter key - autoindent", prevline);
+            txta.insertAfter( "\n" + prevline[1] );
 
         }
+
     },
 
     /*
@@ -419,13 +381,14 @@ var Snipe = new Class({
         if( caret.thin  && (caret.start > 0) ){
 
             var key = txta.getValue().charAt(caret.start-1),
-                snip = this.getSnippet( this.options.directsnips, key );
+                snip = this.directsnips.get( key );
+                //snip = this.getSnippet( this.options.directsnips, key );
 
             if( snip && (snip.snippet == txta.getValue().charAt(caret.start)) ){
 
-                /* remove the closing pair character */
+                // remove the closing pair character
                 txta.setSelectionRange( caret.start, caret.start+1 )
-                    .setSelection('');
+                    .setSelection("");
 
             }
         }
@@ -433,7 +396,7 @@ var Snipe = new Class({
 
     /*
     Function: delete
-        Removes the next TAB (4spaces) if matched
+        Removes the previous TAB (4spaces) if matched
 
     Arguments:
         e - event
@@ -443,12 +406,13 @@ var Snipe = new Class({
     "delete": function(e, txta, caret) {
 
         var tab = this.options.tab;
+            console.log("delete key");
 
         if( caret.thin && !txta.getTillEnd().indexOf(tab) /*index==0*/ ){
 
             e.stop();
             txta.setSelectionRange(caret.start, caret.start + tab.length)
-                .setSelection('');
+                .setSelection("");
 
         }
     },
@@ -458,7 +422,6 @@ var Snipe = new Class({
         Perform tab-completion function.
         Pressing a tab can lead to :
         - expansion of a snippet command cmd and selection of the first parameter
-        - selection of the next snippet parameter (if active snippet)
         - otherwise, expansion to set of spaces (4)
 
 
@@ -470,50 +433,31 @@ var Snipe = new Class({
     */
     tab: function(e, txta, caret){
 
-        var self = this,
-            snips = self.options.snippets,
-            fromStart = txta.getFromStart(),
-            len = fromStart.length,
-            cmd, cmdlen; // ok = false;
+        var self = this, cmd;
 
         e.stop();
 
         if( self.options.tabcompletion ){
 
-            if( self.hasContext() ){
+            if( caret.thin && ( cmd = self.snippets.match() ) ){
 
-                return self.nextAction(txta, caret);
+                //remove the command
+                txta.setSelectionRange(caret.start - cmd.length, caret.start)
+                    .setSelection("");
 
-            }
-
-            if( caret.thin ){
-
-                //lookup the command backwards from the text preceeding the caret
-                for( cmd in snips ){
-
-                    cmdlen = cmd.length;
-
-                    if( (len >= cmdlen) && (cmd == fromStart.slice( - cmdlen )) ){
-
-                        //first remove the command
-                        txta.setSelectionRange(caret.start - cmdlen, caret.start)
-                            .setSelection('');
-
-                        return self.commands.action( cmd );
-
-                    }
-                }
+                //now invoke the corresponding actions
+                self.commands.action( cmd );
             }
 
         }
 
         //if you are still here, convert the tab into spaces
-        self.convertTabToSpaces(e, txta, caret);
+        self.tab2spaces(e, txta, caret);
 
     },
 
     /*
-    Function: convertTabToSpaces
+    Function: tab2spaces
         Convert tabs to spaces. When no snippets are detected, the default
         treatment of the TAB key is to insert a number of spaces.
         Indentation is also applied in case of multi-line selections.
@@ -523,7 +467,7 @@ var Snipe = new Class({
         txta - Textarea object
         caret - caret object, indicating the start/end of the textarea selection
     */
-    convertTabToSpaces: function(e, txta, caret){
+    tab2spaces: function(e, txta, caret){
 
         var tab = this.options.tab,
             selection = txta.getSelection(),
@@ -531,19 +475,19 @@ var Snipe = new Class({
             isCaretAtStart = txta.isCaretAtStartOfLine();
 
         //handle multi-line selection
-        if( selection.indexOf('\n') > -1 ){
+        if( selection.indexOf("\n") > -1 ){
 
-            if( isCaretAtStart ){ selection = '\n' + selection; }
+            if( isCaretAtStart ){ selection = "\n" + selection; }
 
             if( e.shift ){
 
                 //shift-tab: remove leading tab space-block
-                selection = selection.replace(RegExp('\n'+tab,'g'),'\n');
+                selection = selection.replace(RegExp("\n"+tab,"g"),"\n");
 
             } else {
 
                 //tab: auto-indent by inserting a tab space-block
-                selection = selection.replace(/\n/g,'\n'+tab);
+                selection = selection.replace(/\n/g,"\n"+tab);
 
             }
 
@@ -553,11 +497,11 @@ var Snipe = new Class({
 
             if( e.shift ){
 
-                //shift-tab: remove 'backward' tab space-block
-                if( fromStart.test( tab + '$' ) ){
+                //shift-tab: remove "backward" tab space-block
+                if( fromStart.test( tab + "$" ) ){
 
                     txta.setSelectionRange( caret.start - tab.length, caret.start )
-                        .setSelection('');
+                        .setSelection("");
 
                 }
 
@@ -582,450 +526,268 @@ var Snipe = new Class({
         snip - snippet object to make active
     */
     hasContext: function(){
+
         return !!this.context.snip;
+
     },
 
     setContext: function( snip, suggest ){
 
-        this.context = {snip:snip, suggest:suggest};
-        $(this).addClass('activeSnip');
+        //console.log("Snipe.setContext",snip,suggest);
+        this.context = { snip:snip, suggest:suggest };
+        this.toElement().addClass("activeSnip");
 
     },
 
     /*
-    Function: clearContext
+    Function: reset
         Clear the context object, and remove the css class from the textarea.
         Also make sure that no dialogs are left open.
     */
-    clearContext: function(){
+    reset: function(){
 
+        //console.log("Snipe:reset");
         this.context = {};
         this.commands.close();
-        $(this).removeClass('activeSnip').focus();
+        this.toElement().removeClass("activeSnip").focus();
 
     },
 
-    /*
-    Function: getSnippet
-        Retrieve and validate the snippet. Returns false when the snippet is not
-        found or not in scope.
-
-    About snippets:
-    In the simplest case, you can use snippets to insert plain text that you do not
-    want to type again and again. The snippet is expanded when hitting
-    the Tab key: the ''snippet'' is replaced by ''snippet expansion text''.
-
-    (start code)
-    var tabSnippets = {
-        <snippet1> : <snippet expansion text>,
-        <snippet2> : <snippet expansion text>
-    }
-    (end)
-
-    See also [DirectSnippets].
-
-    For example, following snippet will expand the ''toc'' text into the
-    TableOfContents wiki plugin call. Don't forget to escape '{' and '}'
-    with a backslash, because they have a special meaning. (see below)
-    Use the '\n' charater to define multi-line snippets. Start the snippet
-    with '\n' to make sure the snippet starts on a new line.
-
-    (start code)
-    "toc": "\n[\{TableOfContents \}]\n"
-    (end)
-
-    After tab-completion, the caret is placed just after the expanded snippet.
-
-    Snippet parameters:
-    If you want, you can put ''{parameters}'' inside the snippet. Pressing the tab
-    will jump to the next parameter. If you are ok with the default value,
-    just tab over it. If not, start typing to overwrite it.
-
-    (start code)
-    "bold": "__{some bold text}__"
-    (end)
-
-    You can have multiple ''{parameters}'' too. Pressing more tabs will get you there.
-
-    (start code)
-    "link": "[{link text}|{pagename}]"
-    (end)
-
-    Extended snippet syntax:
-    So far we discussed the simple snippet syntax. In order to unlock more advanced
-    snippet features, you'll need to use the extended snippet syntax.
-
-    (start code)
-    "toc": {
-        snippet : "\n[\{TableOfContents \}]\n"
-    }
-    (end)
-
-    which is actually the same as
-
-    (start code)
-    "toc": "\n[\{TableOfContents \}]\n"
-    (end)
-
-    Snippet synonyms:
-    Instead of defining the snippet text, you can also refer to another snippet.
-    This allows you to create synonyms.
 
-    (start code)
-    "allow": {
-        synonym: "acl"
-    }
-    (end)
-
-    Dynamic snippets:
-    Next to static snippet texts, you can also dynamically generate
-    the snippet text through a javascript function. For example, you could
-    use ajax calls to populate the snippet on the fly. The function should return
-    either the string (simple snippet syntax) or a snippet object.
-    (eg return {{ { snippet:"..." } }} )
-
-    (start code)
-    "date": function(e, textarea){
-        return new Date().toLocaleString();
-    }
-    (end)
-
-    or
-
-    (start code)
-    "date": function(e, textarea){
-        var d = new Date().toLocaleString();
-        return { 'snippet': d };
-    }
-    (end)
+    /*
+    Function: smartPairs
+        Direct snippet are invoked immediately when the key is pressed
+        as opposed to a [tabSnippet] which are expanded after pressing the Tab key.
 
-    Snippet scope:
-    See [inScope] to see how to restrict the scope of a snippet.
+        Direct snippets are typically used for smart typing pairs,
+        such as {{ (), [] or {}. }}
+        Direct snippets can also be defined through javascript functions
+        or restricted to a certain scope. (ref. [getSnippet], [inScope] )
 
-    Parameter dialog boxes:
-    To help the entry of parameters, you can specify a predefined set of choices
-    for a ''{parameter}'', as a string (with | separator), js array or js object.
-    A parameter dialog box will be displayed to provide easy selection of
-    one of the choices.  See [Dialog.Selection].
+        First, the snippet is retrieved based on the entered character.
+        Then, the opening- and closing- chars are inserted around the selection.
 
-    Example of parameter suggestion-list:
+    Arguments:
+        e - event
+        txta - Textarea object
+        caret - caret object, indicating the start/end of the textarea selection
 
+    Example:
     (start code)
-    "acl": {
-        snippet: "[\{ALLOW {permission} {principal(,principal)} \}]",
-        permission: "view|edit|modify|comment|rename|upload|delete",
-        "principal(,principal)": "Anonymous|Asserted|Authenticated|All"
-        }
-    }
-    "acl": {
-        snippet: "[\{ALLOW {permission} {principal(,principal)} \}]",
-        permission: [view,edit,modify]
-        }
-    }
-    "acl": {
-        snippet: "[\{ALLOW {permission} {principal(,principal)} \}]",
-        permission: {'Only read access':'view','Read and write access':'edit','R/W, rename, delete access':'modify' }
+    directSnippets: {
+        """ : """,
+        "(" : ")",
+        "{" : "}",
+        "<" : ">",
+        """ : {
+            snippet:""",
+            scope:{
+                "<javascript":"</javascript",
+                "<code":"</code",
+                "<html":"</html"
+            }
         }
     }
     (end)
 
-
-    Arguments:
-        snips - snippet collection object for lookup of the key
-        key - snippet key. If not present, retreive the key from
-            the textarea just to the left of the caret. (i.e. tab-completion)
-
-    Returns:
-        Return a snippet object or false.
-        (start code)
-        returned_object = false || {
-                key: "snippet-key",
-                snippet: " snippet-string ",
-                text: " converted snippet-string, no-parameter braces, auto-indented ",
-                parms: [parm1, parm2, "last-snippet-string" ]
-            }
-        (end)
     */
-    getSnippet: function( snips, cmd ){
-
-        var self = this,
-            txta = self.textarea,
-            fromStart = txta.getFromStart(),
-            snip = snips[cmd],
-            tab = this.options.tab,
-            parms = [],
-            s,last;
-
-        if( snip && snip.synonym ){ snip = snips[snip.synonym]; }
+    smartPairs: function(e, workarea, caret){
 
-        snip = Function.from(snip)(self, [cmd]);
+        var snip, key = e.key;
 
-        if( typeOf(snip) == 'string' ){ snip = { snippet:snip }; }
+        if( this.options.smartpairs && (snip = this.directsnips.get(key)) ){
 
-        if( !snip || !self.inScope(snip, fromStart) ){ return false; }
-
-        s = snip.snippet || '';
-
-        //parse snippet and build the parms[] array with all {parameters}
-        s = s.replace( /\\?\{([^{}]+)\}/g, function(match, name){
-
-            if( match.charAt(0) == '\\' ){ return match.slice(1); }
-            parms.push(name);
-            return name;
-
-        }).replace( /\\\{/g, '{' );
-        //and finally, replace the escaped '\{' by real '{' chars
-
-        //also push the last piece of the snippet onto the parms[] array
-        last = parms.getLast();
-        if(last){ parms.push( s.slice(s.lastIndexOf(last) + last.length) ); }
+            e.stop();
 
-        //collapse \n of previous line if the snippet starts with \n
-        if( s.test(/^\n/) && ( fromStart.test( /(^|[\n\r]\s*)$/ ) ) ) {
-            s = s.slice(1);
-            //console.log("remove leading \\n");
-        }
+            //insert the keystroke, retain the selection and insert the snippet outcome
+            //and keep the original selection (caret)
+            workarea.setSelection( key, workarea.getSelection(), snip.text )
+                    .setSelectionRange( caret.start+1, caret.end+1 );
 
-        //collapse \n of subsequent line when the snippet ends with a \n
-        if( s.test(/\n$/) && ( txta.getTillEnd().test( /^\s*[\n\r]/ ) ) ) {
-            s = s.slice(0,-1);
-            //console.log("remove trailing \\n");
         }
 
-        //auto-indent the snippet's internal newlines \n
-        var prevline = fromStart.split(/\r?\n/).pop(),
-            indent = prevline.match(/^\s+/);
-        if( indent ){ s = s.replace( /\n/g, '\n' + indent[0] ); }
-
-        //complete the snip object
-        snip.text = s;
-        snip.parms = parms;
-
-        return snip;
     },
 
-    /*
-    Function: inScope
-        Sometimes it is useful to restrict the scope of a snippet, and only allow
-        the snippet expansion in specific parts of the text. The scope parameter allows
-        you to do that by defining start and end delimiting strings.
-        For example, the following "fn" snippet will only expands when it appears
-        inside the scope of a script tag.
-
-        (start code)
-        "fn": {
-            snippet: "function( {args} )\{ \n    {body}\n\}\n",
-            scope: {"<script":"</script"} //should be inside this bracket
-        }
-        (end)
 
-        The opposite is possible too. Use the 'nScope' or not-in-scope parameter
-        to make sure the snippet is only inserted when not in scope.
-
-        (start code)
-        "special": {
-            snippet: "{special}",
-            nScope: { "%%(":")" } //should not be inside this bracket
-        },
-        (end)
+    /*
+    Method: suggest
+        Suggestion snippets are dialog-boxes appearing as you type.
+        When clicking items in the suggest dialogs, content is inserted
+        in the textarea.
 
-    Arguments:
-        snip - Snippet Object
-        text - (string) used to check for open scope items
+        snip.suggest => {
+            cmd:  <cmd>,
+            start: <pos>,
+            value: <match-string-before-caret>,
+            xxxtail:  <length-match-after-caret>
+        }
 
-    Returns:
-        True when the snippet is in scope, false otherwise.
     */
-    inScope: function(snip, text){
-
-        var pattern, pos, scope=snip.scope, nscope=snip.nscope;
-
-        if( scope ){
+    suggest: function(e){
 
-            if( typeOf(scope)=='function' ){
+        var self = this, suggest;
 
-                return scope( this.textarea );
-
-            } else {
+        if( this.options.autosuggest ){
 
-                for( pattern in scope ){
+            if ( suggest = this.snippets.matchSuggest() ){
 
-                    pos = text.lastIndexOf(pattern);
-                    if( (pos > -1) && (text.indexOf( scope[pattern], pos ) == -1) ){ return 1 /*true*/; }
-
-                }
-                return false;
-            }
-        }
-
-        if( nscope ){
-
-            for( pattern in nscope ){
-
-                pos = text.lastIndexOf(pattern);
-                if( (pos > -1) && (text.indexOf( nscope[pattern], pos ) == -1) ){ return !1 /*false*/; }
+                console.log( "Snipe.suggest ",suggest );
+                this.setContext( null/*snip*/, suggest );
+                return this.commands.action( suggest.cmd , suggest.pfx );
 
             }
+            //close suggest dialog if one is still open
+            //is this ok??
 
+            this.reset();
         }
-        return 1 /*true*/;
+
     },
 
 
     /*
-    Function: directSnippet
-        Direct snippet are invoked immediately when the key is pressed
-        as opposed to a [tabSnippet] which are expanded after pressing the Tab key.
-
-        Direct snippets are typically used for smart typing pairs,
-        such as {{ (), [] or {}. }}
-        Direct snippets can also be defined through javascript functions
-        or restricted to a certain scope. (ref. [getSnippet], [inScope] )
+    Method: select
+        Selection snippets are dialog-boxes appearing when you select some text.
+        When clicking items in the SELECTION dialogs, content is inserted
+        in the textarea.
 
-        First, the snippet is retrieved based on the entered character.
-        Then, the opening- and closing- chars are inserted around the selection.
-
-    Arguments:
-        e - event
-        txta - Textarea object
-        caret - caret object, indicating the start/end of the textarea selection
-
-    Example:
-    (start code)
-    directSnippets: {
-        '"' : '"',
-        '(' : ')',
-        '{' : '}',
-        "<" : ">",
-        "'" : {
-            snippet:"'",
-            scope:{
-                "<javascript":"</javascript",
-                "<code":"</code",
-                "<html":"</html"
-            }
-        }
-    }
-    (end)
+        Selection commands (lowest priority vs. other snippets)
+        - selectInline: "bold|italic|mono|link",
+        - selectBlock:  "code|prettify"
+        - selectStartOfLine: "!!!h1|!!h2|!h3|bold|italic|mono|link|plugin"
 
     */
-    directSnippet: function(e, txta, caret){
+    select: function(e){
 
         var self = this,
-            options = self.options,
+            txta = self.textarea,
+            selection = txta.getSelectionRange(),
+            cmd = "selectInline";
 
-            snip = self.getSnippet( options.directsnips, e.key );
+        if( !txta.getSelectionRange().thin ){
 
-        if( snip && options.smartpairs ){
+            if( txta.isCaretAtStartOfLine() ){
 
-            e.stop();
+                cmd = txta.isCaretAtEndOfLine() ? "selectBlock" : "selectStartOfLine";
+
+            }
 
-            txta.setSelection( e.key, txta.getSelection(), snip.snippet )
-                .setSelectionRange( caret.start+1, caret.end+1 );
+            console.log("SELECT COMMAND", cmd );
+            //return this.commands.action( cmd );
 
         }
 
     },
 
+
     /*
     Function: action
-        This function executes the proper action.
-        The command can be given throug TAB-completion or by pressing a button.
-
-        It looks up the snippet and inserts its value in the textarea.
-
-        When text was selected prior to the click event, the selection will
-        be injected in one of the snippet {parameter}.
-
-        Additionally, when the snippet only contains one {parameter},
-        the snippet will toggle: i.e. remove the snippet when already present,
-        otherwise insert the snippet.
-
-        TODO:
-        Prior to the insertion of the snippet, the caret will be moved to the beginning of the line.
-        Prior to the insertion of the snippet, the caret will be moved to the beginning of the next line.
+        This function executes the command action.
+        The trigger of a command can be
+        - "keydown/keypress" TAB key , via snipe.commands to close any dialogs...
+        - "keydown" with CTRL/META key, via snipe.command to close any dialogs...
+        - "click" event on a button , captured by snipe.commands
+        - "keyup"/"click" on the workarea -- suggestion snip
+
+
+        It looks up and processes the snippet.
+        - insert the snippet text at the caret in the textarea.
+        - when text was selected (prior to the click or keyup event):
+            - the snippet text will replace the selection
+            - the selection will be passed as a parameter into the snippet
+            - if the snippet has ONE parameter, the snippet text will be toggled:
+              i.e. remove the snippet when already present, otherwise insert the snippet
 
     Arguments:
-        e - (event) keypress or keydown event.
+        cmd - (string) used to loopup the snippet
+        args - (optional) additional snippet arguments (eg value passed via a dialog)
     */
-    action: function( cmd, args ){
+    action: function( cmd ){
 
         var self = this,
+            args = Array.slice(arguments,1),
+            snip, // = self.context.snip || self.getSnippet(self.options.snippets, cmd),
             txta = self.textarea,
             caret = txta.getSelectionRange(),
-            snip = self.context.snip || self.getSnippet(self.options.snippets, cmd),
-            s;
-
-        //console.log("Action: "+cmd+" ("+args+") text=["+snip.text+"] parms=["+snip.parms+"] "+!!snip);
-
-        if( snip ){
-
-            s = snip.text;
+            s, p;
 
-            if( snip.action ){    //eg undo, redo
+        //console.log("Snipe.action "+cmd+" ("+args+") snipText=["+snip.text+"] snipParms=["+snip.parms+"] ");
+        console.log("Snipe:action() "+cmd+" ["+args+"]");
 
-                return snip.action.call(self, cmd, snip, args );
-
-            }
-
-            self.undoredo.onChange();
+        if( snip = self.snippets.get(cmd) ){
 
             if( snip.event ){
-
-                return self.fireEvent(snip.event, [cmd, args]);
-
+                console.log("Snipe:action() fire-event: ",snip.event);
+                return self.fireEvent(snip.event, arguments);
             }
 
+            this.fireEvent("beforeChange");
 
-            $(txta).focus();
+            //$(txta).focus();  CHECKME
 
-            if( self.options.autosuggest && self.context.suggest ){
+            if( snip.suggest ){
 
                 return self.suggestAction( cmd, args );
+                //todo -- bring that here inline ...
+                //snip.text = ...
 
             }
 
+            s = snip.text;
 
             if( !caret.thin && (snip.parms.length==2) ){
+
                 s = self.toggleSnip(txta, snip, caret);
                 //console.log("toggle snippet: "+s+" parms:"+snip.parms);
+
             }
 
             //inject args into the snippet parms
-            if( args ){
-                args.each( function(arg){
-                    if(snip.parms.length > 1){
-                        s = s.replace( snip.parms.shift(), arg );
-                    }
-                });
-                //console.log("inject args: "+s+" "+snip.parms);
-            }
+            while( args && args[0] ){
+
+                if( snip.parms[0] ){
+                    s = s.replace( snip.parms.shift(), args.shift() );
+                }
 
-            //inject selected text into first snippet parm
-            if( !caret.thin && (snip.parms[1] /*length>1*/) ){
-                s = s.replace( snip.parms.shift(), txta.getSelection() );
-                //console.log("inject selection: "+s+" "+snip.parms);
             }
 
-            //now insert the snippet text
-            txta.setSelection( s );
+            //replace the first parm by the selected text
+            if( snip.parms[1] ){
 
-            if( !snip.parms.length/*length==0*/ ){
+                if( !caret.thin ){
+                    p = snip.parms.shift();
+                        s = s.replace( p, txta.getSelection() );
+                    }
 
-                //when no selection, move caret after the inserted snippet,
-                //otherwise leave the selection unchanged
-                if( caret.thin ){ txta.setSelectionRange( caret.start + s.length ); }
-                //console.log("action:: should we clear this ? " + self.hasContext() );
-                self.clearContext();
+            }
 
-            } else {
+            txta.setSelection( s );
 
-                //this snippet has one or more parameters
+            //next action
+            if( false && snip.parms[1] ){
+
+                //this snippet has one or more parameters left
                 //store the active snip and process the next {parameter}
                 //checkme !!
+
                 if( !self.hasContext() ){ self.setContext( snip ); }
+
                 caret = txta.getSelectionRange(); //update new caret
-                self.nextAction(txta, caret);
+
+                self.nextAction( txta, caret );
+
+
+            } else {
+
+                //when no selection, move caret after the inserted snippet,
+                //otherwise leave the selection unchanged
+                if( caret.thin ){
+
+                    txta.setSelectionRange( caret.start + s.length );
+
+                }
+
+                self.reset();
 
             }
 
@@ -1062,9 +824,9 @@ var Snipe = new Class({
             arr = s.trim().split( snip.parms[0] ),
             fst = arr[0],
             lst = arr[1],
-            re = new RegExp( '^\\s*' + fst.trim().escapeRegExp() + '\\s*(.*)\\s*' + lst.trim().escapeRegExp() + '\\s*$' );
+            re = new RegExp( "^\\s*" + fst.trim().escapeRegExp() + "\\s*(.*)\\s*" + lst.trim().escapeRegExp() + "\\s*$" );
 
-        if( (fst+lst)!='' ){
+        if( (fst+lst)!="" ){
 
             s = txta.getSelection();
             snip.parms = [];
@@ -1072,10 +834,10 @@ var Snipe = new Class({
             // if pfx & sfx (with optional whitespace) are matched: remove them
             if( s.test(re) ){
 
-                s = s.replace( re, '$1' );
+                s = s.replace( re, "$1" );
 
             // if pfx/sfx are matched just outside the selection: extend selection
-            } else if( txta.getFromStart().test(fst+'$') && txta.getTillEnd().test('^'+lst) ){
+            } else if( txta.getFromStart().test(fst.escapeRegExp()+"$") && txta.getTillEnd().test("^"+lst.escapeRegExp()) ){
 
                 txta.setSelectionRange(caret.start-fst.length, caret.end+lst.length);
 
@@ -1089,75 +851,35 @@ var Snipe = new Class({
     },
 
     /*
-    Method: suggest
-        Suggestion snippets are dialog-boxes appearing as you type.
-        When clicking items in the suggest dialogs, content is inserted
-        in the textarea.
-
-    */
-    suggest: function(){
-
-        var self = this,
-            txta = self.textarea,
-            caret = txta.getSelectionRange(),
-            fromStart = txta.getFromStart(),
-            suggestions = self.suggestions,
-            cmd, suggest, snip;
-
-        if( !self.options.autosuggest ) return;
-
-        for( cmd in suggestions ){
-
-            snip = suggestions[cmd];
-            suggest = snip.suggest(txta, caret);
-
-            if( suggest /*&& self.inScope(snip, fromStart)*/ ){
-
-                if(!suggest.tail) suggest.tail = 0; //ensure default value
-
-                //console.log( "Suggest: "+ cmd + " [" + JSON.encode(suggest)+"]" );
-                self.setContext( snip, suggest );
-                return self.commands.action(cmd, suggest.match);
-
-            }
-        }
-
-        //if you got here, no suggestions
-        this.clearContext();
-
-    },
-
-    /*
     Method: suggestAction
         <todo>
-        suggest = { start: start-position , match:'string', tail: length }
+        suggest = { pfx: "prefix-string" , match:"full-string" }
     */
-    suggestAction: function( cmd, value ){
+    suggestAction: function( cmd, valueArr ){
 
-        var self = this,
-            txta = self.textarea,
-            suggest = self.context.suggest,
-            end = suggest.start + suggest.match.length + suggest.tail;
+console.log(this.context.suggest);
+        var suggest = this.context.suggest,
+            workarea = this.textarea,
+            start = workarea.getSelectionRange().start - suggest.pfx.length;
 
+        //jump to the matched string
+        start = workarea.indexOf( suggest.match, start );
 
-        //console.log('SuggestAction: '+ cmd+' (' +value + ') [' + JSON.encode(suggest) + ']');
+        if( start >= 0 ){
 
-        //set the selection to the replaceable text, and inject the new value
-        txta.setSelectionRange( suggest.start, end ).setSelection( value );
+            workarea.setSelectionRange( start, start + suggest.match.length )
+                    .setSelection( valueArr )
+                    .setSelectionRange( workarea.getSelectionRange().end );
 
-        //if tail, set the selection on the tail --why ??
-        if( suggest.tail>0 ){
-            txta.setSelectionRange( end - suggest.tail, txta.getSelectionRange().end );
         }
-
-        self.clearContext();
-        return self.suggest();
+        return this.suggest();
 
     },
 
     /*
+    CHECKME : remove..
     Function: nextAction
-        Process the next ''{parameter}'' of the active snippet as you tab along
+        Process the next ""{parameter}"" of the active snippet as you tab along
         or after you clicked a button or closed a dialog.
 
     Arguments:
@@ -1172,7 +894,7 @@ var Snipe = new Class({
             dialog,
             pos;
 
-        while( parms[0] /*.length > 0*/ ){
+        while( parms[0] ){
 
             dialog = parms.shift();
             pos = txta.getValue().indexOf(dialog, caret.start);
@@ -1180,19 +902,20 @@ var Snipe = new Class({
             //console.log("next action: "+dialog+ " pos:" + pos + " parms: "+parms+" caret:"+caret.start);
 
             //found the next {dialog} or possibly the end of the snippet
-            if( (dialog !='') && (pos > -1) ){
+            if( (dialog !="") && (pos > -1) ){
 
-                if( parms[0] /*.length > 0*/ ){
+                if( parms[0] ){
 
                     // select the next {dialog}
                     txta.setSelectionRange( pos, pos + dialog.length );
 
                     //invoke the new dialog
-                    //console.log('next action: invoke '+dialog+" "+snip[dialog])
+                    //console.log("next action: invoke "+dialog+" "+snip[dialog])
                     self.commands.action( dialog, snip[dialog] );
 
                     //remember every selected snippet dialog
-                    self.undoredo.onChange();
+                    //self.undoredo.onChange();
+                    //self.fireEvent("change");
 
                     return; // and retain the context snip for subsequent {dialogs}
 
@@ -1205,22 +928,24 @@ var Snipe = new Class({
             }
         }
 
-        self.clearContext();
+        self.reset();
     },
 
     /*
+    Interface: Undoable
+        Implemenent the "undoable" interface.
+
     Function: getState
-        Return the current state which consist of the content and selection of the textarea.
-        It implements the ''Undoable'' interface called from the [UndoRedo] class.
+        State contains the value of the main and work area, the cursor and scroll position
     */
     getState: function(){
 
         var txta = this.textarea,
-            el = $(txta);
+            el = txta.toElement();
 
         return {
             main: this.mainarea.value,
-            value: el.get('value'),
+            value: el.get("value"),
             cursor: txta.getSelectionRange(),
             scrollTop: el.scrollTop,
             scrollLeft: el.scrollLeft
@@ -1229,24 +954,25 @@ var Snipe = new Class({
 
     /*
     Function: putState
-        Set a state of the Snip editor. This works in conjunction with the [UndoRedo] class.
+        Set a state of the Snip editor. This works in conjunction with the getState function.
 
     Argument:
-        state - object originally created by the getState funcion
+        state - object originally created by the getState function
     */
     putState: function(state){
 
         var self = this,
             txta = self.textarea,
-            el = $(txta);
+            el = txta.toElement();
 
-        self.clearContext();
+        self.reset();
         self.mainarea.value = state.main;
         el.value = state.value;
         el.scrollTop = state.scrollTop;
         el.scrollLeft = state.scrollLeft;
-        txta.setSelectionRange( state.cursor.start, state.cursor.end )
-            .fireEvent('change',[state.value]);
+        txta.setSelectionRange( state.cursor.start, state.cursor.end );
+
+        //self.fireEvent("change");
     }
 
 });

Added: jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Undoable.js
URL: http://svn.apache.org/viewvc/jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Undoable.js?rev=1686927&view=auto
==============================================================================
--- jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Undoable.js (added)
+++ jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Undoable.js Mon Jun 22 20:11:42 2015
@@ -0,0 +1,121 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); fyou may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+/*
+Class: UndoRedo
+    Abstract class implements a simple undo/redo stack to save and restore
+    the state of an "undo-able" object.
+    The object needs to provide a {{getState()}} and a {{putState(obj)}} methods.
+    Prior to any changes, it should fire a "berforChange" event.
+    The undo and redo event trigger the corresponding actions.
+
+
+Options:
+    maxundo - integer , maximal size of the undo and redo stack (default 20)
+    redo - (optional) DOM element, will get a click handler to the redo() function
+    undo - (optional) DOM element, will get a click handler to the undo() function
+
+Example:
+(start code)
+    x = new Class({
+        Implement: [Options, Undoable],
+        initialize:function(...){
+          ..
+          this.initializeUndoable( btns, 10 );
+          ..
+        },
+        ..
+            this.fireEvent("beforeChange");
+            this.fireEvent("undo");
+            this.fireEvent("redo");
+        ..
+        getState: function(){ ... return state-object; },
+        putState: function( state-object ){ ... }
+(end)
+*/
+
+!function(){
+
+// Helper function to change the css style of the undo/redo buttons.
+function btnStyle( self ){
+
+    var btns = self.btns, disabled = "disabled";
+
+    if( btns.undo ){ btns.undo.ifClass( !self.undo[0], disabled); }
+    if( btns.redo ){ btns.redo.ifClass( !self.redo[0], disabled); }
+
+}
+
+// Swap the state of an undoable object.
+function swap(from, to){
+
+    from = this[from]; to = this[to];
+
+    if( from[0] ){
+
+        to.push( this.getState() ); //current state
+        this.putState( from.pop() ); //new state
+
+    }
+
+    btnStyle( this );  //repaint buttons
+
+}
+
+
+this.Undoable = new Class({
+
+    initializeUndoable: function(btns, maxundo){
+
+        var self = this,
+            undo = self.undo = [];
+
+        self.redo = [];
+        self.btns = btns;
+        btnStyle(self); //{ redo:btns.redo, undo:btns.undo };
+        maxundo = maxundo || 40;
+
+        self.addEvents({
+
+            //Persist the current state of the undo-able object.
+            //Calls the {{this.getState()}} to retrieve the state info.
+            //  state - (optional) state object to be persisted. If not present,
+            //          the state will be retrieved via {{this.getState()}} function.
+            beforeChange: function( state ){
+
+                //console.log("Undoable beforeChange:", undo.length);
+
+                undo.push( state || self.getState() );
+                self.redo = [];
+
+                if( undo[maxundo] ){ undo.shift(); }
+
+                btnStyle(self);
+
+            },
+            undo: swap.pass(["undo", "redo"], self),
+            redo: swap.pass(["redo", "undo"], self)
+        });
+
+    }
+
+});
+
+}();

Modified: jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Wiki.Edit.js
URL: http://svn.apache.org/viewvc/jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Wiki.Edit.js?rev=1686927&r1=1686926&r2=1686927&view=diff
==============================================================================
--- jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Wiki.Edit.js (original)
+++ jspwiki/trunk/jspwiki-war/src/main/scripts/wiki-edit/Wiki.Edit.js Mon Jun 22 20:11:42 2015
@@ -20,92 +20,118 @@
 */
 /*
 Class: Wiki.Edit
-    The WikiEdit class implements the JSPWiki's specific editor, with support
+    Wiki.Edit implements the JSPWiki plain editor, with support
     for JSPWIki's markup, suggestion popups, ajax based page preview, etc...
 
-    It uses an enhanced textarea based on the [SnipEditor] class.
+    It uses an enhanced textarea based on the [Snipe] class.
 */
 
 
 !function( wiki ){
 
-var container,
-    textarea, 
-    snipe, 
-    preview, previewcache,
-    sectionDropDown;
-    
-wiki.add('#editform', function( element ){
-
-    container = element;
-    textarea = container.getElement('.editor');
-    preview  = container.getElement('.ajaxpreview');
-    sectionDropDown = container.getElement('[data-sections]');
-    
-    onbeforeunload( textarea );
-    
+var editform,
+    textarea,
+    snipe,
+    preview,
+    previewcache,
+    sectionsDropDown;
+
+wiki.add("#editform", function( element ){
+
+    editform = element;
+    textarea = getFormElement(".editor");
+    preview = getFormElement(".ajaxpreview");
+
+    onbeforeunload( );
+
     snipe = new Snipe( textarea, {
 
+        container: editform,
+        undoBtns: {
+            undo: getFormElement("[data-cmd=undo]"),
+            redo: getFormElement("[data-cmd=redo]")
+        },
         snippets: wiki.Snips,
         directsnips: wiki.DirectSnips,
 
-        onChange: preview ? livepreview : null, 
-        onConfig: config
+        onChange: ( function(){
+            console.log("change event");
+            if( preview ){ livepreview(); }
+        } ).debounce(),
+
+        onChange: livepreview.debounce(),
+
+        onConfig: config  //configuration callback
 
     });
 
+    if( wiki.Context == "edit"
+    && ( sectionsDropDown = getFormElement(".sections") ) ){
 
-    if( wiki.Context == 'edit' && sectionDropDown ){ 
-        new Snipe.Sections( sectionDropDown, {
-           snipe: snipe, 
-           parser: sectionParser
+        new Snipe.Sections( sectionsDropDown, {
+           snipe: snipe,
+           parser: jspwikiSectionParser  //jspwiki section parser callback
         });
+
     }
 
+    resizer( snipe.toElement(), getFormElement(".resizer"), "EditorCookie" );
 
-    resizer( element.getElement('.resizer'), 'EditorCookie' );
- 
-    /*
-    Initialize the configuration checkboxes:
-    set the checkbox according to the wiki-prefs (cookie) and configure the snip-editor.
-    */
-    ['tabcompletion','smartpairs','autosuggest','livepreview','previewcolumn'].each( function(cmd){
+    //Initialize the configuration checkboxes
+    //Read the wiki-prefs cookie values.
+    //EG: tabcompletion, smartpairs, autosuggest, livepreview, previewcolumn..
+    editform.getElements(".config [data-cmd]").each( function( configElement ){
+
+        var cmd = configElement.getAttribute("data-cmd");
+
+        configElement.checked = !!wiki.prefs.get(cmd);  //read wiki preferences cookie
+
+        configElement.getParent().ifClass(configElement.checked, 'active');
+
+        //wiki.Snips[ cmd ] = { event: 'config' };
+        //console.log(cmd, configElement.checked);
+        config(cmd);
+
+    });
 
-        var el = container.getElement('[data-cmd='+cmd+']');
-        if( el ){
-            //console.log('init config ',cmd);
-            el.checked = !!wiki.get(cmd);
-            config( cmd );
-        }
 
-    })
-    
 });
 
 
     /*
+    Function: getFormElement
+        Helper function : lookup first matching descendant DOM element from the editform
+    */
+    function getFormElement( selector ){
+
+        return editform.getElement( selector );
+
+    }
+
+    /*
     Function: onbeforeunload
-        Install an onbeforeunload handler, which is called ''before'' the page unloads.
+        Install an onbeforeunload handler, which is called ""before"" the page unloads.
         The user gets a warning in case the textarea was changed, without saving.
 
         The onbeforeunload handler is automatically removed on regular exit of the page.
     */
-    function onbeforeunload( textarea ){
+    function onbeforeunload( ){
 
         window.onbeforeunload = function(){
-            if( textarea.value != textarea.defaultValue ){ return "edit.areyousure".localize(); }
+            if( textarea.value != textarea.defaultValue ){
+                return "edit.areyousure".localize();
+            }
         };
 
-        textarea.getParent('form').addEvent('submit', function(){
-            window.onbeforeunload = null;
-        });
+        editform.addEvent("submit", function(){ window.onbeforeunload = null; });
     }
 
+
     /*
-    Function: makeResizable
-        Activate the resize handle.
-        While dragging the textarea, also update the size of the
-        preview area. Store the new height in the 'EditorSize' prefs cookie.
+    Function: resizer
+        Activate the resize handle of the input textarea.
+        While dragging the resize handle, update the textarea and the
+        preview area. Store the new height in the "EditorSize" prefs cookie.
 
     Arguments:
         element - draggable resize handle (DOM element)
@@ -116,35 +142,38 @@ wiki.add('#editform', function( element
         textarea - resizable textarea (DOM element)
         preview - preview (DOM element)
     */
-    function resizer( handle, cookie ){
+    function resizer( resizableTextarea, handle, cookie ){
 
-        var height = 'height',
-            textarea = snipe.toElement(),
-            size = wiki.get(cookie),
+        var height = "height",
+            size = wiki.prefs.get(cookie),
             y;
 
-        function dragging(add){ handle.ifClass(add,'dragging'); }
+        function helpdragging(add){ handle.ifClass(add, "dragging"); }
+
+        if( size ){
+
+            resizableTextarea.setStyle(height, size);
+            preview.setStyle(height, size);
 
-        if( size ){ 
-            textarea.setStyle(height, size); 
-            preview.setStyle(height, size); 
         }
 
-        if( handle ){ 
+        if( handle ){
+
             //console.log("resizer ",textarea,preview);
-            textarea.makeResizable({
+            resizableTextarea.makeResizable({
                 handle: handle,
-                modifiers: { x:null },
+                modifiers: { x: null },
                 onDrag: function(){
                     y = this.value.now.y;
                     preview.setStyle(height, y);
-                    wiki.set(cookie, y);
+                    wiki.prefs.set(cookie, y);
                 },
-                onBeforeStart: dragging.pass(true),
-                onComplete: dragging.pass(false),
-                onCancel: dragging.pass(false)
+                onBeforeStart: helpdragging.pass(true),
+                onComplete: helpdragging.pass(false),
+                onCancel: helpdragging.pass(false)
 
             });
+
         }
     }
 
@@ -154,16 +183,18 @@ wiki.add('#editform', function( element
         Linked as onChange handler to the SnipEditor.
         Make AJAX call to the backend to convert the contents of the textarea
         (wiki markup) to HTML.
-        FIXME: should work bothways. wysiwyg <-> wikimarkup
+        TODO: should work bothways. wysiwyg <-> wikimarkup
 
     */
-    function livepreview(v){
+    function livepreview( ){
 
-        var text = snipe.toElement().get('value');
+        var text = snipe.toElement().get("value"),
+            loading = "loading";
 
-        if( !$('livepreview').checked ){
+    console.log("**** change event");
+        if( !(getFormElement("[data-cmd=livepreview]") || {}).checked ){
 
-            //clean preview area
+            //cleanup the preview area
             if( previewcache ){
                 preview.empty();
                 previewcache = null;
@@ -172,8 +203,9 @@ wiki.add('#editform', function( element
         } else if( previewcache != text.length ){
 
             previewcache = text.length;
-            //return preview.set('html',preview.get('html')+' Lorem ipsum'); //test code
+            //return preview.set("html",preview.get("html")+" Lorem ipsum"); //test code
 
+            console.log("**** invoke Request.HTML ",wiki.XHRPreview)
             new Request.HTML({
                 url: wiki.XHRPreview,
                 data: {
@@ -181,10 +213,12 @@ wiki.add('#editform', function( element
                     wikimarkup: text
                 },
                 update: preview,
-                onRequest: function(){ preview.addClass('loading'); },
-                onComplete: function(){ 
-                    preview.removeClass('loading'); 
-                    wiki.update(); 
+                onRequest: function(){ preview.addClass(loading); },
+                onComplete: function(){
+
+                    preview.removeClass(loading);
+                    wiki.update();
+
                 }
             }).send();
 
@@ -207,45 +241,65 @@ wiki.add('#editform', function( element
     */
     function config( cmd ){
 
-        var el = container.getElement('[data-cmd='+cmd+']'),
-            state, editarea; 
+        var el = getFormElement("[data-cmd=" + cmd + "]"),
+            state,
+            previewcontainer;
+
+        //console.log("CONFIG EVENT", cmd, arguments);
 
         if( el ){
-        
-            wiki.set(cmd, state = el.checked);
 
-            if( cmd.test(/livepreview|previewcolumn/) ){
+            state = el.checked;
+            wiki.prefs.set(cmd, state);  //persist in the pref cookie
+
+            if( cmd.test( /livepreview|previewcolumn/ ) ){
+
+                previewcontainer = getFormElement(".edit-area").ifClass(state, cmd);
+
+                if( cmd == "livepreview" ){
+
+                    //disable the previewcolumn toolbar cmd
+                    getFormElement("[data-cmd=previewcolumn]").disabled = !state;
 
-                editarea = container.getElement('.edit-area').ifClass(state,cmd);
+                } else {
 
-                if( cmd == 'livepreview' ){
+                    /* Toggle the position of the preview-area in the dom
 
-                    container.getElement('[data-cmd=previewcolumn]').disabled = !state;
+                    1. HORIZONTAL SIDE BY SIDE VIEW
+                    div.snip
+                        div.toolbar
+                        div.edit-area.livepreview.previewcolumn
+                            div.col-50
+                            div.col-50.ajaxpreview
+                        div.resizer
 
-                } else {    //cmd == 'previewcolumn'
+                    2. VERTICAL VIEW
+                    div.snip
+                        div.toolbar
+                        div.edit-area.livepreview
+                            div.col-50
+                        div.resizer
+                        div.col-50.ajaxpreview
+                    */
+
+                    if( !state ){ previewcontainer = previewcontainer.getParent(); }
+                    previewcontainer.grab(preview);
 
-                    if(state){ 
-                        editarea.adopt(preview); 
-                    } else {
-                        preview.inject( container.getElement('.resizer'), 'after'); 
-                    }
                 }
             }
-        
-            snipe.set(cmd, state).fireEvent('change');
+
+            snipe.set(cmd, state).fireEvent("change");
 
         }
     }
 
-        
+
     /*
-    Function: sectionParser
-        Convert a jspwiki-markup page to an array of page sections.
-        Sections are marked with a JSPWiki header line. ( !, !! !!! )
-
-        This function is a callback function for the [SnipEditor].
-        It is called by [snipeditor.buildToc] every time the textarea of the
-        snipeditor is being changed.
+    Function: jspwikiSectionParser
+        Convert a jspwiki-markup page into an array of page sections.
+        Sections are marked by jspwiki headers:  !, !!  or !!!
+
+        This function is used as a callback for [Snip.Sections]
 
     Returns:
         This function returns a array of objects [{title, start, depth}]
@@ -253,44 +307,51 @@ wiki.add('#editform', function( element
         start - (number) offset within the text string where this section starts
         depth - (number) nesting level of the section 0,1...n
     */
-    function sectionParser( text ){
+    function jspwikiSectionParser( text ){
 
         var result = [],
-            DELIM = '\u00a4',
+            DELIM = "\u00a4",
 
             tt = text
-                // mask any header markup inside a {{{ ... }}} but keep length of the text unchanged!
+
+                // mask confusing header markup inside a {{{ ... }}} but keep length of the text unchanged!
                 .replace(/\{\{\{([\s\S]*?)\}\}\}/g, function(match){
-                    return match.replace( /^!/mg, ' ' );
+                    return match.replace( /^!/mg, " " );
                 })
+
                 // break string up into array of headers and section-bodies :
                 // [0] : text prior to the first header
                 // [1,odd] : header markup !, !! or !!!
                 // [2,even] : remainder of the section, starting with header title
-                .replace( /^([!]{1,3})/mg, DELIM+"$1"+DELIM )
+                .replace( /^([!]{1,3})/mg, DELIM + "$1" + DELIM )
+
                 .split(DELIM),
 
             pos = tt.shift().length,  //get length of the first element, prior to first section
             count = tt.length,
             i, hlen, title;
 
-        for( i=0; i<count; i=i+2 ){
+        for( i = 0; i < count; i = i + 2 ){
 
             hlen = tt[i].length;
             //take first line
-            title = tt[i+1].split(/[\r\n]/)[0]
-                //remove unescaped(~) inline wiki markup __,'',{{,}}, %%(*), /%
-                .replace(/(^|[^~])(__|''|\{\{|\}\}|%%\([^\)]+\)|%%\S+\s|%%\([^\)]+\)|\/%)/g,'$1')
+            title = tt[i + 1].split(/[\r\n]/)[0]
+
+                //remove unescaped(~) inline wiki markup __,"",{{,}}, %%(*), /%
+                .replace(/(^|[^~])(__|""|\{\{|\}\}|%%\([^\)]+\)|%%\S+\s|%%\([^\)]+\)|\/%)/g, "$1")
+
                 //and remove wiki-markup escape chars ~
-                .replace(/~([^~])/g, '$1');
+                .replace(/~([^~])/g, "$1");
 
             //depth: convert length of header markup (!!!,!!,!) into #depth-level:  3,2,1 => 0,1,2
-            result.push({ title:title, start:pos, depth:3-hlen });
-            pos += hlen + tt[i+1].length;
+            result.push({ title: title, start: pos, depth: 3 - hlen });
+            pos += hlen + tt[i + 1].length;
+
         }
 
         return result;
+
     }
 
 
-}(Wiki);
+}( Wiki );



Mime
View raw message