[Scodoc-devel] [SVN] Scolar : [1530] Tags sur les modules

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Jeu 14 Juil 23:58:11 CEST 2016


Une pièce jointe HTML a été nettoyée...
URL: <https://www-rt.iutv.univ-paris13.fr/pipermail/scodoc-devel/attachments/20160714/ba43bf8a/attachment-0001.html>
-------------- section suivante --------------
Modified: branches/ScoDoc7/ZNotes.py
===================================================================
--- branches/ScoDoc7/ZNotes.py	2016-07-14 12:52:59 UTC (rev 1529)
+++ branches/ScoDoc7/ZNotes.py	2016-07-14 21:58:11 UTC (rev 1530)
@@ -61,6 +61,7 @@
 import sco_edit_formation
 import sco_edit_matiere
 import sco_edit_module
+import sco_tag_module
 import sco_bulletins
 import sco_bulletins_pdf
 import sco_recapcomplet
@@ -293,6 +294,11 @@
     edit_module_set_code_apogee = sco_edit_module.edit_module_set_code_apogee
     security.declareProtected(ScoView, 'module_list')
     module_list = sco_edit_module.module_list
+    # Tags
+    security.declareProtected(ScoView, 'module_tag_search')
+    module_tag_search=sco_tag_module.module_tag_search
+    security.declareProtected(ScoChangeFormation,'module_tag_set')
+    module_tag_set=sco_tag_module.module_tag_set
     
     # 
     security.declareProtected(ScoView, 'index_html')

Modified: branches/ScoDoc7/sco_edit_module.py
===================================================================
--- branches/ScoDoc7/sco_edit_module.py	2016-07-14 12:52:59 UTC (rev 1529)
+++ branches/ScoDoc7/sco_edit_module.py	2016-07-14 21:58:11 UTC (rev 1530)
@@ -34,6 +34,7 @@
 import sco_codes_parcours
 from TrivialFormulator import TrivialFormulator, TF
 import sco_formsemestre
+import sco_tag_module
 
 _MODULE_HELP = """<p class="help">
 Les modules sont décrits dans le programme pédagogique. Un module est pour ce 
@@ -162,10 +163,20 @@
     
     dest_url = REQUEST.URL1 + '/ue_list?formation_id=' + Mod['formation_id']
 
-    H = [ context.sco_header(REQUEST, page_title="Modification du module %(titre)s" % Mod),
-          """<h2>Modification du module %(titre)s""" % Mod,
-          """ (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
-          _MODULE_HELP ]
+    H = [ 
+        context.sco_header(
+            REQUEST, 
+            page_title="Modification du module %(titre)s" % Mod,
+            cssstyles=['libjs/jQuery-tagEditor/jquery.tag-editor.css'],
+            javascripts=[ 
+                'libjs/jQuery-tagEditor/jquery.tag-editor.min.js',
+                'libjs/jQuery-tagEditor/jquery.caret.min.js',
+                'js/module_tag_editor.js' ]
+            ),
+        """<h2>Modification du module %(titre)s""" % Mod,
+        """ (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
+        _MODULE_HELP 
+        ]
     if not unlocked:
         H.append("""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>""")
     
@@ -175,7 +186,7 @@
                        }),
         ('titre'    , { 'size' : 30, 'explanation' : 'nom du module' }),
         ('abbrev'    , { 'size' : 20, 'explanation' : 'nom abrégé (pour bulletins)' }),
-
+        
         ('heures_cours' , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de cours' }),
         ('heures_td'    , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de Travaux Dirigés' }),
         ('heures_tp'    , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de Travaux Pratiques' }),
@@ -198,8 +209,13 @@
         ('numero',    { 'size' : 2, 'explanation' : 'numéro (1,2,3,4...) pour ordre d\'affichage',
                         'type' : 'int' }),        
         ),
-                           initvalues = Mod,
-                           submitlabel = 'Modifier ce module')
+        html_foot_markup = """<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
+            module_id,
+            ','.join(sco_tag_module.module_tag_list(context, module_id))
+            ),
+        initvalues = Mod,
+        submitlabel = 'Modifier ce module')    
+    
     if tf[0] == 0:
         return '\n'.join(H) + tf[1] + context.sco_footer(REQUEST)
     elif tf[0] == -1:

Modified: branches/ScoDoc7/sco_edit_ue.py
===================================================================
--- branches/ScoDoc7/sco_edit_ue.py	2016-07-14 12:52:59 UTC (rev 1529)
+++ branches/ScoDoc7/sco_edit_ue.py	2016-07-14 21:58:11 UTC (rev 1530)
@@ -37,6 +37,7 @@
 import sco_formsemestre
 import sco_formsemestre_validation
 import sco_codes_parcours
+import sco_tag_module
 
 
 def ue_create(context, formation_id=None, REQUEST=None):
@@ -226,7 +227,14 @@
     delete_disabled_icon = icontag('delete_small_dis_img', title="Suppression impossible (module utilisé)")
     H = [ context.sco_header(
         REQUEST, 
-        javascripts=[ 'libjs/jinplace-1.2.1.min.js', 'js/ue_list.js' ],
+        cssstyles=['libjs/jQuery-tagEditor/jquery.tag-editor.css'],
+        javascripts=[ 
+            'libjs/jinplace-1.2.1.min.js', 
+            'js/ue_list.js',
+            'libjs/jQuery-tagEditor/jquery.tag-editor.min.js',
+            'libjs/jQuery-tagEditor/jquery.caret.min.js',
+            'js/module_tag_editor.js',
+            ],
         page_title="Programme %s" % F['acronyme']),
         """<h2>Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s""" % F,
         lockicon, '</h2>' 
@@ -255,8 +263,11 @@
         H.append('<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>' )
     H.append('<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>' % F )
     H.append('</div>')
+    
     # Description des UE/matières/modules
     H.append('<div class="ue_list_tit">Programme pédagogique:</div>')
+
+    H.append('<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>')
     
     ue_list = context.do_ue_list( args={ 'formation_id' : formation_id } )
     # tri par semestre et numero:
@@ -348,7 +359,9 @@
                 else:
                     klass =''
                 heurescoef += ', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s">' % (klass, Mod['module_id']) + (Mod['code_apogee'] or APO_MISSING_CODE_STR) + '</span>'
-                H.append(' (%s %s)' % (parcours.SESSION_NAME, Mod['semestre_id']) + ' (%s)' % heurescoef )
+                tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></form></span>"""
+                tag_edit = tag_mk.format( Mod['module_id'], ','.join(sco_tag_module.module_tag_list(context, Mod['module_id'])))
+                H.append(' %s %s' % (parcours.SESSION_NAME, Mod['semestre_id']) + ' (%s)' % heurescoef + tag_edit )
                 H.append('</li>')
             if not Modlist:
                 H.append('<li>Aucun module dans cette matière !')

Modified: branches/ScoDoc7/sco_tag_module.py
===================================================================
--- branches/ScoDoc7/sco_tag_module.py	2016-07-14 12:52:59 UTC (rev 1529)
+++ branches/ScoDoc7/sco_tag_module.py	2016-07-14 21:58:11 UTC (rev 1530)
@@ -45,6 +45,11 @@
 #  + creer un tag (nom)
 #  + lier un tag à un module
 #  + enlever un tag d'un module (si c'est le dernier, supprimer le tag lui même)
+#
+# API publiée:
+#   module_tag_list_all  -> tous les noms d etags (pour l'autocomplete)
+#   module_tag_list( module_id ) -> les noms de tags associés à ce module
+#   module_tag_set( module_id, taglist ) -> modifie les tags
 
 class ModuleTag:
     def __init__(self, context, title, module_id=''):
@@ -60,6 +65,7 @@
             self.tag_id = r[0]['tag_id']
         else:
             # Create new tag:
+            log('creating new tag: %s' % self.title)
             cnx = context.GetDBConnexion()
             oid = DBInsertDict(cnx, 'notes_tags', { 'title' : self.title }, commit=True)
             self.tag_id = SimpleDictFetch(
@@ -81,6 +87,7 @@
     
     def tag_module(self, module_id):
         """Associate tag to module"""
+        log('tagging module %s with %s' % (module_id, self.title))
         args = { 'module_id' : module_id, 'tag_id' : self.tag_id }
         r = SimpleDictFetch(
             self.context, 
@@ -97,6 +104,7 @@
         If no more modules tagged with this tag, delete it.
         Return True if Tag still exists.
         """
+        log('removing tag %s from %s' % (self.title, module_id))
         args = { 'module_id' : module_id, 'tag_id' : self.tag_id }
         SimpleQuery(
             self.context,
@@ -135,13 +143,81 @@
                 AND f.formation_code = %(formation_code)s
                 ''', args)
         return [ x['module_id'] for x in r ]
-        
-def tag_list_all(context):
+
+# API
+
+VALID_EXP = re.compile('^[a-zA-Z0-9_\-]+$')
+def module_tag_search(context, term, REQUEST=None):
     """List all used tag names (for auto-completion)"""
-    r = SimpleDictFetch(context, "SELECT title FROM notes_tags", {})
+    # restrict charset to avoid injections
+    log('module_tag_search %s' % term)
+    if not VALID_EXP.match(term):
+        data = []
+    else:
+        r = SimpleDictFetch(
+            context, 
+            "SELECT title FROM notes_tags WHERE title LIKE %(term)s", 
+            {'term' : term+'%'})
+        data = [ x['title'] for x in r ]
+    log(data)
+    return sendJSON(REQUEST, data)
+
+def module_tag_list(context, module_id=''):
+    """les noms de tags associés à ce module
+    """
+    r = SimpleDictFetch(context, '''SELECT t.title
+          FROM notes_modules_tags mt, notes_tags t
+          WHERE mt.tag_id = t.tag_id
+          AND mt.module_id = %(module_id)s
+          ''', { 'module_id' : module_id } )
     return [ x['title'] for x in r ]
 
+def module_tag_set(context, module_id='', taglist=''):
+    """
+    """
+    taglist = taglist.strip().split(',')
+    log('module_tag_set: module_id=%s taglist=%s' % (module_id, taglist))
+    # Sanity check:
+    Mod= context.do_module_list( args={ 'module_id' : module_id } )
+    if not Mod:
+        raise ScoValueError('invalid module !')
 
+    newtags = set(taglist)
+    oldtags = set(module_tag_list(context, module_id))
+    to_del = oldtags - newtags
+    to_add = newtags - oldtags
+
+    # should be atomic, but it's not.
+    for tagname in to_add:
+        t = ModuleTag(context, tagname, module_id=module_id)
+    for tagname in to_del:
+        t = ModuleTag(context, tagname)
+        t.remove_tag_from_module(module_id)
+
+
+def get_etud_tagged_modules(context, etudid, tagname):
+    """Liste d'infos sur les modules de ce semestre avec ce tag.
+    Cherche dans tous les semestres dans lesquel l'étudiant est ou a été inscrit.
+    Construit la liste des moduels avec le tag donné par tagname    
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    R = []
+    for sem in etud['sems']:
+        nt = context._getNotesCache().get_NotesTable(context, sem['formsemestre_id'])
+        modimpls = nt.get_modimpls()
+        for modimpl in modimpls:
+            tags = module_tag_list(context, module_id=modimpl['module_id'])
+            if tagname in tags:
+                moy = nt.get_etud_mod_moy(modimpl['moduleimpl_id'], etudid) # ou NI si non inscrit
+                R.append( {
+                    'sem' : sem,
+                    'moy' : moy, # valuer réelle, ou NI (non inscrit au module ou NA0 (pas de note)
+                    'moduleimpl' : modimpl,
+                    'tags' : tags
+                    } )
+    return R
+                
+          
 """Tests:
 from debug import *
 from sco_tag_module import *
@@ -157,5 +233,19 @@
 t.list_modules()
 t.list_modules(formation_code='ccc') # empty list
 t.list_modules(formation_code='FCOD2')
-tag_list_all(context)
+
+
+Un essai de get_etud_tagged_modules:
+from debug import *
+from sco_tag_module import *
+context = go_dept(app, 'GEA').Notes
+
+etudid='GEAEID80687'
+etud = self.getEtudInfo(etudid=etudid, filled=True)[0]
+sem = etud['sems'][0]
+
+[ tm['moy'] for tm in get_etud_tagged_modules(context, etudid, 'allo') ]
+
+# si besoin après modif par le Web:
+# context._inval_cache()
 """

Modified: branches/ScoDoc7/static/css/scodoc.css
===================================================================
--- branches/ScoDoc7/static/css/scodoc.css	2016-07-14 12:52:59 UTC (rev 1529)
+++ branches/ScoDoc7/static/css/scodoc.css	2016-07-14 21:58:11 UTC (rev 1530)
@@ -2403,5 +2403,20 @@
 
 /* Editable */
 span.span_apo_edit {
-  border-bottom: 1px dashed #84ae84;
-}
\ No newline at end of file
+    border-bottom: 1px dashed #84ae84;
+}
+
+/* Tags */
+.notes_module_list span.sco_tag_edit {    
+    display: none;
+}
+span.sco_tag_edit .tag-editor {
+    background-color: rgb(210,210,210);
+    border: 0px;
+    margin-left: 40px;
+    margin-top: 2px;
+}
+
+span.sco_tag_edit .tag-editor-delete {
+    height: 20px;
+}

Added: branches/ScoDoc7/static/js/module_tag_editor.js
===================================================================
--- branches/ScoDoc7/static/js/module_tag_editor.js	                        (rev 0)
+++ branches/ScoDoc7/static/js/module_tag_editor.js	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,30 @@
+// Edition tags sur modules
+
+
+$(function() {
+    $('.module_tag_editor').tagEditor({
+        initialTags: '',
+        placeholder: 'Tags du module ...',
+        onChange: function(field, editor, tags) {
+            $.post('module_tag_set', 
+                   {
+                       module_id: field.data("module_id"),
+                       taglist: tags.join()
+                   });
+        },
+        autocomplete: {
+            delay: 0, // show suggestions immediately
+            position: { collision: 'flip' }, // automatic menu position up/down
+            source: "module_tag_search"
+        },
+    });
+
+    $('.sco_tag_checkbox').click(function() {
+        if( $(this).is(':checked')) {
+            $(".sco_tag_edit").show();
+        } else {
+            $(".sco_tag_edit").hide();
+        }
+    }); 
+
+});
\ No newline at end of file

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/bower.json
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/bower.json	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/bower.json	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,33 @@
+{
+    "name": "jquery-tag-editor",
+	"description": "A powerful and lightweight tag editor plugin for jQuery.",
+    "version": "1.0.20",
+    "dependencies": {
+        "jquery": ">=1.7",
+        "caret": null
+    },
+    "homepage": "https://github.com/Pixabay/jQuery-tagEditor",
+    "authors": [{
+        "name": "Simon Steinberger",
+        "url": "https://pixabay.com/users/Simon/",
+        "email": "simon at pixabay.com"
+    }],
+    "keywords": [
+        "tags",
+        "keywords",
+        "editor",
+        "drag and drop",
+        "editable",
+        "edit"
+    ],
+    "licenses": [{
+        "type": "MIT",
+        "url": "http://www.opensource.org/licenses/mit-license.php"
+    }],
+    "ignore": [
+        "bower.json",
+        "demo.html",
+        "readme.md",
+        "tag-editor.jquery.json"
+    ]
+}

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/demo.html
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/demo.html	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/demo.html	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,461 @@
+<!DOCTYPE html><html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>jQuery tagEditor Plugin</title>
+    <meta name="description" content="A lightweight and sophisticated tag editor for jQuery. Sortable, editable tags with cursor navigation, autocomplete, and callbacks.">
+    <link rel="shortcut icon" href="https://pixabay.com/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300">
+    <link rel="stylesheet" href="https://cdn.rawgit.com/yahoo/pure-release/v0.6.0/pure-min.css">
+    <style>
+        body { margin: 0; padding: 0; border: 0; min-width: 320px; color: #777; }
+        html, button, input, select, textarea, .pure-g [class *= "pure-u"] { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1.02em; }
+        p, td { line-height: 1.5; }
+        ul { padding: 0 0 0 20px; }
+
+        th { background: #eee; white-space: nowrap; }
+        th, td { padding: 10px; text-align: left; vertical-align: top; font-size: .9em; font-weight: normal; border-right: 1px solid #fff; }
+        td:first-child { white-space: nowrap; color: #008000; width: 1%; font-style: italic; }
+
+        h1, h2, h3 { color: #4b4b4b; font-family: "Source Sans Pro", sans-serif; font-weight: 300; margin: 0 0 1.2em; }
+        h1 { font-size: 4.5em; color: #1f8dd6; margin: 0 0 .4em; }
+        h2 { font-size: 2em; color: #636363; }
+        h3 { font-size: 1.8em; color: #4b4b4b; margin: 1.8em 0 .8em }
+        h4 { font: bold 1em sans-serif; color: #636363; margin: 4em 0 1em; }
+        a { color: #4e99c7; text-decoration: none; }
+        a:hover { text-decoration: underline; }
+        p, pre { margin: 0 0 1.2em; }
+        ::selection { color: #fff; background: #328efd; }
+        ::-moz-selection { color: #fff; background: #328efd; }
+
+        @media (max-width:480px) {
+            h1 { font-size: 3em; }
+            h2 { font-size: 1.8em; }
+            h3 { font-size: 1.5em; }
+            td:first-child { white-space: normal; }
+        }
+
+        .inline-code { padding: 1px 5px; background: #eee; border-radius: 2px; }
+        pre { padding: 15px 10px; font-size: .9em; color: #555; background: #edf3f8; }
+        pre i { color: #aaa; } /* comments */
+        pre b { font-weight: normal; color: #cf4b25; } /* strings */
+        pre em { color: #0c59e9; } /* numeric */
+
+        /* Pure CSS */
+        .pure-button { margin: 5px 0; text-decoration: none !important; }
+        .button-lg { margin: 5px 0; padding: .65em 1.6em; font-size: 105%; }
+        .button-sm { font-size: 85%; }
+
+        textarea {
+            width: 100%; height: 29px; padding: .3em .5em; border: 1px solid #ddd; font-size: .9em;
+            box-sizing: border-box; margin: 0 0 20px;
+        }
+        textarea[readonly] { color: #aaa; background: #f7f7f7; }
+
+        #response {
+            margin: 0 0 1.2em; padding: 10px; background: #f3f3f3; color: #777;
+            font-size: .9em; max-height: 150px; overflow: hidden; overflow-y: auto;
+        }
+        #response i { font-style: normal; color: #cf4b25; }
+        #response hr { margin: 2px 0; border: 0; border-top: 1px solid #eee; border-bottom: 1px solid #fdfdfd; }
+
+        /* overwrite default CSS for tiny, dark tags in demo5 */
+        #demo5+.tag-editor { background: #fafafa; font-size: 12px; }
+        #demo5+.tag-editor .tag-editor-tag { color: #fff; background: #555; border-radius: 2px; }
+        #demo5+.tag-editor .tag-editor-spacer { width: 7px; }
+        #demo5+.tag-editor .tag-editor-delete { display: none; }
+
+        /* color tags */
+        .tag-editor .red-tag .tag-editor-tag { color: #c65353; background: #ffd7d7; }
+        .tag-editor .red-tag .tag-editor-delete { background-color: #ffd7d7; }
+        .tag-editor .green-tag .tag-editor-tag { color: #45872c; background: #e1f3da; }
+        .tag-editor .green-tag .tag-editor-delete { background-color: #e1f3da; }
+    </style>
+    <link rel="stylesheet" href="jquery.tag-editor.css">
+</head>
+<body>
+    <div style="max-width:900px;padding:0 10px;margin:40px auto;text-align:center">
+        <h1>tagEditor</h1>
+        <h2>A powerful and lightweight tag editor plugin for jQuery.</h2>
+        <a href="https://github.com/Pixabay/jQuery-tagEditor/archive/master.zip" class="pure-button pure-button-primary button-lg">Download</a>
+         
+        <a href="https://github.com/Pixabay/jQuery-tagEditor" class="pure-button button-lg">View on GitHub</a>
+    </div>
+    <div style="border-top: 1px solid #eee;border-bottom:1px solid #eee;background:#fafafa;margin:30px 0;padding:20px 5px">
+        <div style="padding :0 7px 0 5px;max-width:900px;margin:auto">
+            <textarea id="hero-demo">example tags, sortable, autocomplete, edit in place, tab/cursor navigation, duplicate check, callbacks, copy-paste, placeholder, public methods, custom delimiter, graceful degradation</textarea>
+        </div>
+    </div>
+    <div style="max-width:900px;margin:auto;padding:0 10px 50px">
+        <h3>Overview and Features</h3>
+        <p>
+            Released under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a>.
+            Source on <a href="https://github.com/Pixabay/jQuery-tagEditor">Github</a> (<a href="https://github.com/Pixabay/jQuery-tagEditor#changelog">changelog</a>).
+            Compatible with jQuery 1.7.0+ in Firefox, Safari, Chrome, Opera, Internet Explorer 8+. IE7 technically works, but no care has gone into CSS/layout bugs.
+            tagEditor depends on accursoft's <a href="https://github.com/accursoft/caret">caret plugin</a> (1.1 kB minified).
+        </p>
+        <ul>
+            <li>Lightweight: 8.5 kB of JavaScript - less than 3.2 kB gzipped</li>
+            <li>Edit in place tags</li>
+            <li>Intuitive navigation between tags with cursor keys, Tab, Shift+Tab, Enter, Pos1, End, Backspace, Del, and ESC</li>
+            <li>Optional jQuery UI sortable</li>
+            <li>Optional jQuery UI autocomplete</li>
+            <li>Copy-paste or delete multiple selected tags</li>
+            <li>Duplicate tags check</li>
+            <li>Custom delimiter/s</li>
+            <li>Placeholder</li>
+            <li>Custom style for faulty tags</li>
+            <li>Public methods for reading, adding and removing tags + destroy function</li>
+            <li>Callbacks</li>
+            <li>Allows tabindex for form navigation</li>
+            <li>Graceful degradation if JavaScript is disabled</li>
+        </ul>
+        <p>
+            This plugin was developed by and for <a href="https://pixabay.com/">Pixabay.com</a> - an international repository for free Public Domain images.
+            We have implemented this piece of software in production and we share it - in the spirit of Pixabay - freely with others.
+        </p>
+
+        <h3>Usage</h3>
+        <p>
+            Include the stylesheet <span class="inline-code">jquery.tag-editor.css</span> in the <span class="inline-code"><head></span> section of your HTML document - and the JavaScript file <span class="inline-code">jquery.tag-editor.min.js</span> after loading jQuery and optional jQuery UI sortable/autocomplete.
+            Make sure to also load accursoft's <a href="http://code.accursoft.com/caret">caret plugin</a> (1.1 kB minified).
+            tagEditor accepts settings from an object of key/value pairs, and can be assigned to any text input field or textarea.
+        </p>
+        <pre>
+$(selector).tagEditor({key1: value1, key2: value2});
+
+<i>// examples</i>
+
+<i>// assign tag editor to textarea - existing text will be used as initial tags</i>
+$(<b>'textarea'</b>).tagEditor();
+
+<i>// assign tag editor to text input with initial tags</i>
+$(<b>'input[type="text"]'</b>).tagEditor({ initialTags: [<b>'tag1'</b>, <b>'tag2'</b>, <b>'tag3'</b>] });
+
+<i>// use jQuery UI autocomplete</i>
+$(<b>'#my_textarea'</b>).tagEditor({ autocomplete: { <b>'source'</b>: <b>'/url/'</b>, minLength: <em>3</em> } });</pre>
+
+        <h3>Settings</h3>
+        <table>
+            <tr><th>Property</th><th>Default</th><th>Description</th></tr>
+            <tr><td>initialTags</td><td>[]</td><td>Initial tags as an array of strings.</td></tr>
+            <tr><td>maxTags</td><td><i>null</i></td><td>Maximum number of allowed tags.</td></tr>
+            <tr><td>maxLength</td><td>50</td><td><span class="inline-code">maxlength</span> attribute of the tag input field.</td></tr>
+            <tr>
+                <td>delimiter</td><td style="white-space:nowrap">',;'</td>
+                <td>
+                    <p>
+                        Required string of delimiters - characters for separating tags.
+                        The first character is used as default delimiter in the (hidden) original field.
+                    </p>
+                </td>
+            </tr>
+            <tr><td>placeholder</td><td>''</td><td>Placeholder text for empty tag editor.</td></tr>
+            <tr><td>forceLowercase</td><td><i>true</i></td><td>Lowercase all tags.</td></tr>
+            <tr><td>removeDuplicates</td><td><i>true</i></td><td>Automatically remove duplicate tags.</td></tr>
+            <tr><td>clickDelete</td><td><i>false</i></td><td>Delete tags on right click and on Ctrl+click.</td></tr>
+            <tr><td>animateDelete</td><td><i>175</i></td><td>Animate duration for deletion of tags in milliseconds. Set to 0 for non-animated removal.</td></tr>
+            <tr><td>sortable</td><td><i>true</i></td><td>If <a href="https://jqueryui.com/sortable/">jQuery UI sortable</a> is available and this option is set to <span class="inline-code">true</span>, tags are sortable by drag and drop.</td></tr>
+            <tr><td>autocomplete</td><td><i>null</i></td><td><a href="https://jqueryui.com/autocomplete/">jQuery UI autocomplete</a> options as key/value pairs object. If provided, jQuery UI autocomplete must be loaded additionally.</td></tr>
+
+            <tr><td colspan="3"> </td></tr>
+            <tr><th>Callbacks</th><th colspan="2"></th></tr>
+            <tr><td>onChange(field, editor, tags)</td><td colspan="2">Callback that fires after tags are changed. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element (an <ul> list of tag elements), and <span class="inline-code">tags</span> contains the list of current tags.</td></tr>
+            <tr><td>beforeTagSave(field, editor, tags, tag, val)</td><td colspan="2">Callback that fires before a tag is saved. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element. <span class="inline-code">tags</span> contains the list of current tags, <span class="inline-code">tag</span> is the value that is about to get overwritten (empty string, unless an existing tag gets changed), and <span class="inline-code">val</span> is the new value to be saved. <span class="inline-code">beforeTagSave()</span> may return a string for overwriting the saved tag. Return <span class="inline-code">false</span> for reverting to the tag's previous value (or to skip this tag value in the case of copy-paste insertion).</td></tr>
+            <tr><td>beforeTagDelete(field, editor, tags, val)</td><td colspan="2">Callback that fires before a tag is deleted. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element. <span class="inline-code">tags</span> contains the list of current tags, <span class="inline-code">val</span> is the tag that is about to get deleted. Return <span class="inline-code">false</span> to prevent this action.</td></tr>
+
+            <tr><td colspan="3"> </td></tr>
+            <tr><th>Public Methods</th><th colspan="2"></th></tr>
+            <tr><td>getTags</td><td colspan="2">
+                Returns a list of objects in the following format:
+                <br>[{ field: <i>selected input/textarea</i>, editor: <i>editor instance for field</i>, tags: <i>current tags</i> }]
+            </td></tr>
+            <tr><td>addTag(val, blur)</td><td colspan="2">Adds <span class="inline-code">val</span> as a new tag. Set <span class="inline-code">blur</span> to <span class="inline-code">true</span> if focus should not be set automatically into an empty, new tag after this action.</td></tr>
+            <tr><td>removeTag(val, blur)</td><td colspan="2">Removes <span class="inline-code">val</span> as tag. Set <span class="inline-code">blur</span> to <span class="inline-code">true</span> if focus should not be set automatically into an empty, new tag after this action.</td></tr>
+            <tr><td>destroy</td><td colspan="2">Removes the tag editor instance an restores visibility of the original text field or textarea.</td></tr>
+        </table>
+
+        <h3 style="margin-top:.8em;border-top:1px solid #eee;padding-top:1.8em">Demos</h3>
+
+        <h4 style="margin-top:.5em">Basic settings</h4>
+        <pre>
+$(<b>'#demo1'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>, <b>'Example'</b>, <b>'Tags'</b>],
+    delimiter: <b>', '</b>, <i>/* space and comma */</i>
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em">
+            <p>The original field - textarea or text input - is normally hidden automatically. We show it here to make value changes visible:</p>
+            <textarea id="demo1"></textarea>
+        </div>
+        <p>
+            The placeholder is visible when all tags are deleted and the editor looses focus.
+            jQuery UI is already loaded on this page - and by default, tags are then sortable via drag and drop.
+        </p>
+
+        <h4>Autocomplete</h4>
+        <p>
+            For enabling tag autocompletion, make sure to have <a href="https://jqueryui.com/autocomplete/">jQuery UI autocomplete</a> readily loaded.
+            You can then pass <i>any</i> options that work with UI autocomplete to your tagEditor settings.
+        </p>
+        <pre>
+$(<b>'#demo2'</b>).tagEditor({
+    autocomplete: {
+        delay: 0, <i>// show suggestions immediately</i>
+        position: { collision: 'flip' }, <i>// automatic menu position up/down</i>
+        source: [<b>'ActionScript'</b>, <b>'AppleScript'</b>, <b>'Asp'</b>, ... <b>'Python'</b>, <b>'Ruby'</b>]
+    },
+    forceLowercase: <em>false</em>,
+    placeholder: <b>'Programming languages ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo2"></textarea></div>
+
+        <h4>Public methods</h4>
+        <pre>
+$(<b>'#demo3'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>],
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo3"></textarea></div>
+        <p>
+            <span onclick="alert($('#demo3').tagEditor('getTags')[0].tags);" class="ed_on pure-button button-sm">getTags</span>
+            <span onclick="$('#demo3').tagEditor('addTag', 'example');" class="ed_on pure-button button-sm">addTag 'example'</span>
+            <span onclick="$('#demo3').tagEditor('removeTag', 'example', true);" class="ed_on pure-button button-sm">removeTag 'example'</span>
+            <span id="remove_all_tags" class="ed_on pure-button button-sm">Remove all tags</span>
+            <span onclick="$('#demo3').tagEditor('destroy');$('.ed_on').hide();$('.ed_off').show();" class="ed_on pure-button button-sm">destroy</span>
+            <span onclick="$('#demo3').tagEditor({ placeholder: 'Enter tags ...' });$('.ed_off').hide();$('.ed_on').show();" class="ed_off pure-button button-sm" style="display:none">Init editor</span>
+        </p>
+        <pre>
+<i>// actions on button clicks</i>
+
+<i>// getTags</i>
+alert( $(<b>'#demo3'</b>).tagEditor(<b>'getTags'</b>)[0].tags );
+
+<i>// addTag</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'addTag'</b>, <b>'example'</b>);
+
+<i>// removeTag</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'removeTag'</b>, <b>'example'</b>);
+
+<i>// Remove all tags</i>
+function() {
+    var tags = $(<b>'#demo3'</b>).tagEditor(<b>'getTags'</b>)[0].tags;
+    for (i = 0; i < tags.length; i++) { $(<b>'#demo3'</b>).tagEditor(<b>'removeTag'</b>, tags[i]); }
+}
+<i>// working shortcut for removing all tags
+// $('#demo3').next('.tag-editor').find('.tag-editor-delete').click();</i>
+
+<i>// destroy</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'destroy'</b>);
+
+<i>// re-init editor</i>
+$(<b>'#demo3'</b>).tagEditor({ placeholder: <b>'Enter tags ...'</b> });</pre>
+
+        <h4>Callbacks</h4>
+        <pre>
+$(<b>'#demo4'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>],
+    placeholder: <b>'Enter tags ...'</b>,
+    onChange: function(field, editor, tags) {
+        $(<b>'#response'</b>).prepend(
+            <b>'Tags changed to: '</b> + (tags.length ? tags.join(<b>', '</b>) : <b>'----'</b>) + <b>'<hr>'</b>
+        );
+    },
+    beforeTagSave: function(field, editor, tags, tag, val) {
+        $(<b>'#response'</b>).prepend(<b>'Tag '</b> + val + <b>' saved'</b> + (tag ? <b>' over '</b> + tag : <b>''</b>) + <b>'.'</b>);
+    },
+    beforeTagDelete: function(field, editor, tags, val) {
+        var q = confirm(<b>'Remove tag "'</b> + val + <b>'"?'</b>);
+        if (q) $(<b>'#response'</b>).prepend(<b>'Tag '</b> + val + <b>' deleted.'</b>);
+        else $(<b>'#response'</b>).prepend(<b>'Removal of '</b> + val + <b>' discarded.'</b>);
+        return q;
+    }
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo4"></textarea></div>
+        <p style="font-size:.9em;margin:0 0 .2em">Callback response:</p>
+        <div id="response">Starting tags: <i>hello, world</i></div>
+
+        <h4>Custom style and clickDelete</h4>
+        <p>
+            Use right mouse click or Ctrl+left click to delete tags.
+        </p>
+        <pre>
+$(<b>'#demo5'</b>).tagEditor({
+    clickDelete: true,
+    initialTags: [ ... ],
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo5"></textarea></div>
+        <pre>
+<i>/* overwrite default CSS for tiny, dark tags */</i>
+
+<b>#demo5+.tag-editor</b> { background: <em>#fafafa</em>; font-size: <em>12px</em>; }
+<b>#demo5+.tag-editor .tag-editor-tag</b> {
+    color: <em>#fff</em>; background: <em>#555</em>;
+    border-radius: <em>2px</em>;
+}
+<b>#demo5+.tag-editor .tag-editor-spacer</b> { width: <em>7px</em>; }
+<b>#demo5+.tag-editor .tag-editor-delete</b> { display: <em>none</em>; }</pre>
+        <p>
+            This jQuery plugin was designed with custom styling in mind. In this example we've enabled the <span class="inline-code">clickDelete</span> feature while hiding all delete icons. Both options may be used at the same time, as well.
+            By fiddling around with the default stylesheet, you can achieve almost any desired look for your tag Editor.
+            Comments inside the CSS file will help you understand what rule controls which object inside the editor.
+        </p>
+
+        <h4>Custom CSS classes for tags</h4>
+        <p>
+            Using the onChange callback for adding custom CSS classes to specific tags.
+        </p>
+        <pre>
+$(<b>'#demo6'</b>).tagEditor({
+    initialTags: [<b>'custom'</b>, <b>'class'</b>, <b>'red'</b>, <b>'green'</b>, <b>'demo'</b>],
+    onChange: tag_classes
+});
+
+function tag_classes(field, editor, tags) {
+    $(<b>'li'</b>, editor).each(function(){
+        var li = $(this);
+        if (li.find(<b>'.tag-editor-tag'</b>).html() == <b>'red'</b>) li.addClass(<b>'red-tag'</b>);
+        else if (li.find(<b>'.tag-editor-tag'</b>).html() == <b>'green'</b>) li.addClass(<b>'green-tag'</b>)
+        else li.removeClass(<b>'red-tag green-tag'</b>);
+    });
+}
+
+<i>// first assign tag classes after initializing tagEditor; onChange is not called on init</i>
+tag_classes(null, $('#demo6').tagEditor('getTags')[0].editor);</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo6"></textarea></div>
+        <p>
+            In the onChange callback we iterate over all tags and assign custom CSS classes where appropriate.
+            The DOM structure of the editor looks like this:
+        </p>
+        <pre><ul>
+    <li>
+        <div class=<b>"tag-editor-spacer"</b>></div>
+        <div class=<b>"tag-editor-tag"</b>>Tag content</div>
+        <div class=<b>"tag-editor-delete"</b>><i></i></div>
+    </li>
+    [...]
+</ul></pre>
+
+        <p>
+            In the example, we simply add CSS classes to the <span class="inline-code"><li></span> elements.
+            This is just an exampe of what the onChange callback may be used for. Inside of it, <span class="inline-code">addTag</span> and <span class="inline-code">removeTag</span> may be called to dynamically change the current list of tags.
+        </p>
+
+        <div style="margin:40px 0;overflow:hidden">
+            <span id="github_social"></span>
+            <div style="float:left;margin-right:35px">
+                <a href="#" data-width="70" class="twitter-share-button" data-text="jQuery tagEditor Plugin"></a>
+            </div>
+            <div style="float:left">
+                <div class="g-plusone" data-size="medium"></div>
+            </div>
+            <div style="float:left;width:140px" class="fb-like" data-send="false" data-layout="button_count" data-width="140" data-show-faces="false"></div>
+        </div>
+
+        <p style="border-top:1px solid #eee;padding-top:30px">Please report any bugs and issues at the <a href="https://github.com/Pixabay/jQuery-tagEditor">GitHub repositiory</a>.</p>
+        <p>This software is released as Open Source under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a> by <a href="https://pixabay.com/users/Simon/">Simon Steinberger / Pixabay.com</a>.</p>
+
+    </div>
+
+    <div style="background:#fafafa;border-top:1px solid #eee;padding:15px;font-size:.9em">
+        <div style="max-width:900px;margin:auto;padding:0 10px">
+            <a style="float:right;margin-left:20px" href="https://pixabay.com/en/service/about/">About Us</a>
+            <a style="float:right;margin-left:20px" href="https://pixabay.com/en/blog/">Blog</a>
+            <a style="float:right;margin-left:20px" href="https://goodies.pixabay.com/">More Goodies</a>
+            © <a href="https://pixabay.com/">Pixabay.com</a> / Simon Steinberger / Hans Braxmeier
+        </div>
+    </div>
+
+    <div id="fb-root"></div>
+    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
+    <script src="https://code.jquery.com/ui/1.10.2/jquery-ui.min.js"></script>
+    <script src="jquery.caret.min.js"></script>
+    <script src="jquery.tag-editor.js"></script>
+    <script>
+        // jQuery UI autocomplete extension - suggest labels may contain HTML tags
+        // github.com/scottgonzalez/jquery-ui-extensions/blob/master/src/autocomplete/jquery.ui.autocomplete.html.js
+        (function($){var proto=$.ui.autocomplete.prototype,initSource=proto._initSource;function filter(array,term){var matcher=new RegExp($.ui.autocomplete.escapeRegex(term),"i");return $.grep(array,function(value){return matcher.test($("<div>").html(value.label||value.value||value).text());});}$.extend(proto,{_initSource:function(){if(this.options.html&&$.isArray(this.options.source)){this.source=function(request,response){response(filter(this.options.source,request.term));};}else{initSource.call(this);}},_renderItem:function(ul,item){return $("<li></li>").data("item.autocomplete",item).append($("<a></a>")[this.options.html?"html":"text"](item.label)).appendTo(ul);}});})(jQuery);
+
+        var cache = {};
+        function googleSuggest(request, response) {
+            var term = request.term;
+            if (term in cache) { response(cache[term]); return; }
+            $.ajax({
+                url: 'https://query.yahooapis.com/v1/public/yql',
+                dataType: 'JSONP',
+                data: { format: 'json', q: 'select * from xml where url="http://google.com/complete/search?output=toolbar&q='+term+'"' },
+                success: function(data) {
+                    var suggestions = [];
+                    try { var results = data.query.results.toplevel.CompleteSuggestion; } catch(e) { var results = []; }
+                    $.each(results, function() {
+                        try {
+                            var s = this.suggestion.data.toLowerCase();
+                            suggestions.push({label: s.replace(term, '<b>'+term+'</b>'), value: s});
+                        } catch(e){}
+                    });
+                    cache[term] = suggestions;
+                    response(suggestions);
+                }
+            });
+        }
+
+        $(function() {
+            $('#hero-demo').tagEditor({
+                placeholder: 'Enter tags ...',
+                autocomplete: { source: googleSuggest, minLength: 3, delay: 250, html: true, position: { collision: 'flip' } }
+            });
+
+            $('#demo1').tagEditor({ initialTags: ['Hello', 'World', 'Example', 'Tags'], delimiter: ', ', placeholder: 'Enter tags ...' }).css('display', 'block').attr('readonly', true);
+
+            $('#demo2').tagEditor({
+                autocomplete: { delay: 0, position: { collision: 'flip' }, source: ['ActionScript', 'AppleScript', 'Asp', 'BASIC', 'C', 'C++', 'CSS', 'Clojure', 'COBOL', 'ColdFusion', 'Erlang', 'Fortran', 'Groovy', 'Haskell', 'HTML', 'Java', 'JavaScript', 'Lisp', 'Perl', 'PHP', 'Python', 'Ruby', 'Scala', 'Scheme'] },
+                forceLowercase: false,
+                placeholder: 'Programming languages ...'
+            });
+
+            $('#demo3').tagEditor({ initialTags: ['Hello', 'World'], placeholder: 'Enter tags ...' });
+            $('#remove_all_tags').click(function() {
+                var tags = $('#demo3').tagEditor('getTags')[0].tags;
+                for (i=0;i<tags.length;i++){ $('#demo3').tagEditor('removeTag', tags[i]); }
+            });
+
+            $('#demo4').tagEditor({
+                initialTags: ['Hello', 'World'],
+                placeholder: 'Enter tags ...',
+                onChange: function(field, editor, tags) { $('#response').prepend('Tags changed to: <i>'+(tags.length ? tags.join(', ') : '----')+'</i><hr>'); },
+                beforeTagSave: function(field, editor, tags, tag, val) { $('#response').prepend('Tag <i>'+val+'</i> saved'+(tag ? ' over <i>'+tag+'</i>' : '')+'.<hr>'); },
+                beforeTagDelete: function(field, editor, tags, val) {
+                    var q = confirm('Remove tag "'+val+'"?');
+                    if (q) $('#response').prepend('Tag <i>'+val+'</i> deleted.<hr>');
+                    else $('#response').prepend('Removal of <i>'+val+'</i> discarded.<hr>');
+                    return q;
+                }
+            });
+
+            $('#demo5').tagEditor({ clickDelete: true, initialTags: ['custom style', 'dark tags', 'delete on click', 'no delete icon', 'hello', 'world'], placeholder: 'Enter tags ...' });
+
+            function tag_classes(field, editor, tags) {
+                $('li', editor).each(function(){
+                    var li = $(this);
+                    if (li.find('.tag-editor-tag').html() == 'red') li.addClass('red-tag');
+                    else if (li.find('.tag-editor-tag').html() == 'green') li.addClass('green-tag')
+                    else li.removeClass('red-tag green-tag');
+                });
+            }
+            $('#demo6').tagEditor({ initialTags: ['custom', 'class', 'red', 'green', 'demo'], onChange: tag_classes });
+            tag_classes(null, $('#demo6').tagEditor('getTags')[0].editor); // or editor == $('#demo6').next()
+        });
+
+        if (~window.location.href.indexOf('http')) {
+            (function() {var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;po.src = 'https://apis.google.com/js/plusone.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);})();
+            (function(d, s, id) {var js, fjs = d.getElementsByTagName(s)[0];if (d.getElementById(id)) return;js = d.createElement(s); js.id = id;js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.4&appId=114593902037957";fjs.parentNode.insertBefore(js, fjs);}(document, 'script', 'facebook-jssdk'));
+            !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
+            $('#github_social').html('\
+                <iframe style="float:left;margin-right:15px" src="//ghbtns.com/github-btn.html?user=Pixabay&repo=jQuery-tagEditor&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>\
+                <iframe style="float:left;margin-right:15px" src="//ghbtns.com/github-btn.html?user=Pixabay&repo=jQuery-tagEditor&type=fork&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>\
+            ');
+        }
+    </script>
+</body>
+</html>

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.caret.min.js
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.caret.min.js	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.caret.min.js	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,2 @@
+// http://code.accursoft.com/caret - 1.3.3
+!function(e){e.fn.caret=function(e){var t=this[0],n="true"===t.contentEditable;if(0==arguments.length){if(window.getSelection){if(n){t.focus();var o=window.getSelection().getRangeAt(0),r=o.cloneRange();return r.selectNodeContents(t),r.setEnd(o.endContainer,o.endOffset),r.toString().length}return t.selectionStart}if(document.selection){if(t.focus(),n){var o=document.selection.createRange(),r=document.body.createTextRange();return r.moveToElementText(t),r.setEndPoint("EndToEnd",o),r.text.length}var e=0,c=t.createTextRange(),r=document.selection.createRange().duplicate(),a=r.getBookmark();for(c.moveToBookmark(a);0!==c.moveStart("character",-1);)e++;return e}return t.selectionStart?t.selectionStart:0}if(-1==e&&(e=this[n?"text":"val"]().length),window.getSelection)n?(t.focus(),window.getSelection().collapse(t.firstChild,e)):t.setSelectionRange(e,e);else if(document.body.createTextRange)if(n){var c=document.body.createTextRange();c.moveToElementText(t),c.moveStart("character",e),c
 .collapse(!0),c.select()}else{var c=t.createTextRange();c.move("character",e),c.select()}return n||t.focus(),e}}(jQuery);

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.css
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.css	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.css	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,45 @@
+/* surrounding tag container */
+.tag-editor {
+    list-style-type: none; padding: 0 5px 0 0; margin: 0; overflow: hidden; border: 1px solid #eee; cursor: text;
+    font: normal 14px sans-serif; color: #555; background: #fff; line-height: 20px;
+}
+
+/* core styles usually need no change */
+.tag-editor li { display: block; float: left; overflow: hidden; margin: 3px 0; }
+.tag-editor div { float: left; padding: 0 4px; }
+.tag-editor .placeholder { padding: 0 8px; color: #bbb; }
+.tag-editor .tag-editor-spacer { padding: 0; width: 8px; overflow: hidden; color: transparent; background: none; }
+.tag-editor input {
+    vertical-align: inherit; border: 0; outline: none; padding: 0; margin: 0; cursor: text;
+    font-family: inherit; font-weight: inherit; font-size: inherit; font-style: inherit;
+    box-shadow: none; background: none; color: #444;
+}
+/* hide original input field or textarea visually to allow tab navigation */
+.tag-editor-hidden-src { position: absolute !important; left: -99999px; }
+/* hide IE10 "clear field" X */
+.tag-editor ::-ms-clear { display: none; }
+
+/* tag style */
+.tag-editor .tag-editor-tag {
+    padding-left: 5px; color: #46799b; background: #e0eaf1; white-space: nowrap;
+    overflow: hidden; cursor: pointer; border-radius: 2px 0 0 2px;
+}
+
+/* delete icon */
+.tag-editor .tag-editor-delete { background: #e0eaf1; cursor: pointer; border-radius: 0 2px 2px 0; padding-left: 3px; padding-right: 4px; }
+.tag-editor .tag-editor-delete i { line-height: 18px; display: inline-block; }
+.tag-editor .tag-editor-delete i:before { font-size: 16px; color: #8ba7ba; content: "×"; font-style: normal; }
+.tag-editor .tag-editor-delete:hover i:before { color: #d65454; }
+.tag-editor .tag-editor-tag.active+.tag-editor-delete, .tag-editor .tag-editor-tag.active+.tag-editor-delete i { visibility: hidden; cursor: text; }
+
+.tag-editor .tag-editor-tag.active { background: none !important; }
+
+/* jQuery UI autocomplete - code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css */
+.ui-autocomplete { position: absolute; top: 0; left: 0; cursor: default; font-size: 14px; }
+.ui-front { z-index: 9999; }
+.ui-menu { list-style: none; padding: 1px; margin: 0; display: block; outline: none; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.4; min-height: 0; /* support: IE7 */ }
+.ui-widget-content { border: 1px solid #bbb; background: #fff; color: #555; }
+.ui-widget-content a { color: #46799b; }
+.ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { background: #e0eaf1; }
+.ui-helper-hidden-accessible { display: none; }

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.js
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.js	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.js	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,370 @@
+/*
+	jQuery tagEditor v1.0.20
+    Copyright (c) 2014 Simon Steinberger / Pixabay
+    GitHub: https://github.com/Pixabay/jQuery-tagEditor
+	License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+(function($){
+    // auto grow input (stackoverflow.com/questions/931207)
+    $.fn.tagEditorInput=function(){var t=" ",e=$(this),n=parseInt(e.css("fontSize")),i=$("<span/>").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:e.css("fontSize"),fontFamily:e.css("fontFamily"),fontWeight:e.css("fontWeight"),letterSpacing:e.css("letterSpacing"),whiteSpace:"nowrap"}),s=function(){if(t!==(t=e.val())){i.text(t);var s=i.width()+n;20>s&&(s=20),s!=e.width()&&e.width(s)}};return i.insertAfter(e),e.bind("keyup keydown focus",s)};
+
+    // plugin with val as parameter for public methods
+    $.fn.tagEditor = function(options, val, blur){
+
+        // helper
+        function escape(tag) {
+            return tag.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
+        }
+
+        // build options dictionary with default values
+        var blur_result, o = $.extend({}, $.fn.tagEditor.defaults, options), selector = this;
+
+        // store regex and default delimiter in options for later use
+        o.dregex = new RegExp('['+o.delimiter.replace('-', '\-')+']', 'g');
+
+        // public methods
+        if (typeof options == 'string') {
+            // depending on selector, response may contain tag lists of multiple editor instances
+            var response = [];
+            selector.each(function(){
+                // the editor is the next sibling to the hidden, original field
+                var el = $(this), o = el.data('options'), ed = el.next('.tag-editor');
+                if (options == 'getTags')
+                    response.push({field: el[0], editor: ed, tags: ed.data('tags')});
+                else if (options == 'addTag') {
+                    if (o.maxTags && ed.data('tags').length >= o.maxTags) return false;
+                    // insert new tag
+                    $('<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>').appendTo(ed).find('.tag-editor-tag')
+                        .html('<input type="text" maxlength="'+o.maxLength+'">').addClass('active').find('input').val(val).blur();
+                    if (!blur) ed.click();
+                    else $('.placeholder', ed).remove();
+                } else if (options == 'removeTag') {
+                    // trigger delete on matching tag, then click editor to create a new tag
+                    $('.tag-editor-tag', ed).filter(function(){return $(this).text()==val;}).closest('li').find('.tag-editor-delete').click();
+                    if (!blur) ed.click();
+                } else if (options == 'destroy') {
+                    el.removeClass('tag-editor-hidden-src').removeData('options').off('focus.tag-editor').next('.tag-editor').remove();
+                }
+            });
+            return options == 'getTags' ? response : this;
+        }
+
+        // delete selected tags on backspace, delete, ctrl+x
+        if (window.getSelection) $(document).off('keydown.tag-editor').on('keydown.tag-editor', function(e){
+            if (e.which == 8 || e.which == 46 || e.ctrlKey && e.which == 88) {
+                try {
+                    var sel = getSelection(), el = document.activeElement.tagName == 'BODY' ? $(sel.getRangeAt(0).startContainer.parentNode).closest('.tag-editor') : 0;
+                } catch(e){ el = 0; }
+                if (sel.rangeCount > 0 && el && el.length) {
+                    var tags = [], splits = sel.toString().split(el.prev().data('options').dregex);
+                    for (i=0; i<splits.length; i++){ var tag = $.trim(splits[i]); if (tag) tags.push(tag); }
+                    $('.tag-editor-tag', el).each(function(){
+                        if (~$.inArray($(this).text(), tags)) $(this).closest('li').find('.tag-editor-delete').click();
+                    });
+                    return false;
+                }
+            }
+        });
+
+        return selector.each(function(){
+            var el = $(this), tag_list = []; // cache current tags
+
+            // create editor (ed) instance
+            var ed = $('<ul '+(o.clickDelete ? 'oncontextmenu="return false;" ' : '')+'class="tag-editor"></ul>').insertAfter(el);
+            el.addClass('tag-editor-hidden-src') // hide original field
+                .data('options', o) // set data on hidden field
+                .on('focus.tag-editor', function(){ ed.click(); }); // simulate tabindex
+
+            // add dummy item for min-height on empty editor
+            ed.append('<li style="width:1px"> </li>');
+
+            // markup for new tag
+            var new_tag = '<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>';
+
+            // helper: update global data
+            function set_placeholder(){
+                if (o.placeholder && !tag_list.length && !$('.deleted, .placeholder, input', ed).length)
+                    ed.append('<li class="placeholder"><div>'+o.placeholder+'</div></li>');
+            }
+
+            // helper: update global data
+            function update_globals(init){
+                var old_tags = tag_list.toString();
+                tag_list = $('.tag-editor-tag:not(.deleted)', ed).map(function(i, e) {
+                    var val = $.trim($(this).hasClass('active') ? $(this).find('input').val() : $(e).text());
+                    if (val) return val;
+                }).get();
+                ed.data('tags', tag_list);
+                el.val(tag_list.join(o.delimiter[0]));
+                // change callback except for plugin init
+                if (!init) if (old_tags != tag_list.toString()) o.onChange(el, ed, tag_list);
+                set_placeholder();
+            }
+
+            ed.click(function(e, closest_tag){
+                var d, dist = 99999, loc;
+
+                // do not create tag when user selects tags by text selection
+                if (window.getSelection && getSelection() != '') return;
+
+                if (o.maxTags && ed.data('tags').length >= o.maxTags) { ed.find('input').blur(); return false; }
+
+                blur_result = true
+                $('input:focus', ed).blur();
+                if (!blur_result) return false;
+                blur_result = true
+
+                // always remove placeholder on click
+                $('.placeholder', ed).remove();
+                if (closest_tag && closest_tag.length)
+                    loc = 'before';
+                else {
+                    // calculate tag closest to click position
+                    $('.tag-editor-tag', ed).each(function(){
+                        var tag = $(this), to = tag.offset(), tag_x = to.left, tag_y = to.top;
+                        if (e.pageY >= tag_y && e.pageY <= tag_y+tag.height()) {
+                            if (e.pageX < tag_x) loc = 'before', d = tag_x - e.pageX;
+                            else loc = 'after', d = e.pageX - tag_x - tag.width();
+                            if (d < dist) dist = d, closest_tag = tag;
+                        }
+                    });
+                }
+
+                if (loc == 'before') {
+                    $(new_tag).insertBefore(closest_tag.closest('li')).find('.tag-editor-tag').click();
+                } else if (loc == 'after')
+                    $(new_tag).insertAfter(closest_tag.closest('li')).find('.tag-editor-tag').click();
+                else // empty editor
+                    $(new_tag).appendTo(ed).find('.tag-editor-tag').click();
+                return false;
+            });
+
+            ed.on('click', '.tag-editor-delete', function(e){
+                // delete icon is hidden when input is visible; place cursor near invisible delete icon on click
+                if ($(this).prev().hasClass('active')) { $(this).closest('li').find('input').caret(-1); return false; }
+
+                var li = $(this).closest('li'), tag = li.find('.tag-editor-tag');
+                if (o.beforeTagDelete(el, ed, tag_list, tag.text()) === false) return false;
+                tag.addClass('deleted').animate({width: 0}, o.animateDelete, function(){ li.remove(); set_placeholder(); });
+                update_globals();
+                return false;
+            });
+
+            // delete on right mouse click or ctrl+click
+            if (o.clickDelete)
+                ed.on('mousedown', '.tag-editor-tag', function(e){
+                    if (e.ctrlKey || e.which > 1) {
+                        var li = $(this).closest('li'), tag = li.find('.tag-editor-tag');
+                        if (o.beforeTagDelete(el, ed, tag_list, tag.text()) === false) return false;
+                        tag.addClass('deleted').animate({width: 0}, o.animateDelete, function(){ li.remove(); set_placeholder(); });
+                        update_globals();
+                        return false;
+                    }
+                });
+
+            ed.on('click', '.tag-editor-tag', function(e){
+                // delete on right click or ctrl+click -> exit
+                if (o.clickDelete && (e.ctrlKey || e.which > 1)) return false;
+
+                if (!$(this).hasClass('active')) {
+                    var tag = $(this).text();
+                    // guess cursor position in text input
+                    var left_percent = Math.abs(($(this).offset().left - e.pageX)/$(this).width()), caret_pos = parseInt(tag.length*left_percent),
+                        input = $(this).html('<input type="text" maxlength="'+o.maxLength+'" value="'+escape(tag)+'">').addClass('active').find('input');
+                        input.data('old_tag', tag).tagEditorInput().focus().caret(caret_pos);
+                    if (o.autocomplete) {
+                        var aco = $.extend({}, o.autocomplete);
+                        // extend user provided autocomplete select method
+                        var ac_select = 'select'  in aco ? o.autocomplete.select : '';
+                        aco.select = function(e, ui){ if (ac_select) ac_select(e, ui); setTimeout(function(){
+                            ed.trigger('click', [$('.active', ed).find('input').closest('li').next('li').find('.tag-editor-tag')]);
+                        }, 20); };
+                        input.autocomplete(aco);
+                    }
+                }
+                return false;
+            });
+
+            // helper: split into multiple tags, e.g. after paste
+            function split_cleanup(input){
+                var li = input.closest('li'), sub_tags = input.val().replace(/ +/, ' ').split(o.dregex),
+                    old_tag = input.data('old_tag'), old_tags = tag_list.slice(0), exceeded = false, cb_val; // copy tag_list
+                for (var i=0; i<sub_tags.length; i++) {
+                    tag = $.trim(sub_tags[i]).slice(0, o.maxLength);
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    cb_val = o.beforeTagSave(el, ed, old_tags, old_tag, tag);
+                    tag = cb_val || tag;
+                    if (cb_val === false || !tag) continue;
+                    // remove duplicates
+                    if (o.removeDuplicates && ~$.inArray(tag, old_tags))
+                        $('.tag-editor-tag', ed).each(function(){ if ($(this).text() == tag) $(this).closest('li').remove(); });
+                    old_tags.push(tag);
+                    li.before('<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag">'+escape(tag)+'</div><div class="tag-editor-delete"><i></i></div></li>');
+                    if (o.maxTags && old_tags.length >= o.maxTags) { exceeded = true; break; }
+                }
+                input.attr('maxlength', o.maxLength).removeData('old_tag').val('')
+                if (exceeded) input.blur(); else input.focus();
+                update_globals();
+            }
+
+            ed.on('blur', 'input', function(e){
+                e.stopPropagation();
+                var input = $(this), old_tag = input.data('old_tag'), tag = $.trim(input.val().replace(/ +/, ' ').replace(o.dregex, o.delimiter[0]));
+                if (!tag) {
+                    if (old_tag && o.beforeTagDelete(el, ed, tag_list, old_tag) === false) {
+                        input.val(old_tag).focus();
+                        blur_result = false;
+                        update_globals();
+                        return;
+                    }
+                    try { input.closest('li').remove(); } catch(e){}
+                    if (old_tag) update_globals();
+                }
+                else if (tag.indexOf(o.delimiter[0])>=0) { split_cleanup(input); return; }
+                else if (tag != old_tag) {
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    cb_val = o.beforeTagSave(el, ed, tag_list, old_tag, tag);
+                    tag = cb_val || tag;
+                    if (cb_val === false) {
+                        if (old_tag) {
+                            input.val(old_tag).focus();
+                            blur_result = false;
+                            update_globals();
+                            return;
+                        }
+                        try { input.closest('li').remove(); } catch(e){}
+                        if (old_tag) update_globals();
+                    }
+                    // remove duplicates
+                    else if (o.removeDuplicates)
+                        $('.tag-editor-tag:not(.active)', ed).each(function(){ if ($(this).text() == tag) $(this).closest('li').remove(); });
+                }
+                input.parent().html(escape(tag)).removeClass('active');
+                if (tag != old_tag) update_globals();
+                set_placeholder();
+            });
+
+            var pasted_content;
+            ed.on('paste', 'input', function(e){
+                $(this).removeAttr('maxlength');
+                pasted_content = $(this);
+                setTimeout(function(){ split_cleanup(pasted_content); }, 30);
+            });
+
+            // keypress delimiter
+            var inp;
+            ed.on('keypress', 'input', function(e){
+                if (o.delimiter.indexOf(String.fromCharCode(e.which))>=0) {
+                    inp = $(this);
+                    setTimeout(function(){ split_cleanup(inp); }, 20);
+                }
+            });
+
+            ed.on('keydown', 'input', function(e){
+                var $t = $(this);
+
+                // left/up key + backspace key on empty field
+                if ((e.which == 37 || !o.autocomplete && e.which == 38) && !$t.caret() || e.which == 8 && !$t.val()) {
+                    var prev_tag = $t.closest('li').prev('li').find('.tag-editor-tag');
+                    if (prev_tag.length) prev_tag.click().find('input').caret(-1);
+                    else if ($t.val() && !(o.maxTags && ed.data('tags').length >= o.maxTags)) $(new_tag).insertBefore($t.closest('li')).find('.tag-editor-tag').click();
+                    return false;
+                }
+                // right/down key
+                else if ((e.which == 39 || !o.autocomplete && e.which == 40) && ($t.caret() == $t.val().length)) {
+                    var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                    if (next_tag.length) next_tag.click().find('input').caret(0);
+                    else if ($t.val()) ed.click();
+                    return false;
+                }
+                // tab key
+                else if (e.which == 9) {
+                    // shift+tab
+                    if (e.shiftKey) {
+                        var prev_tag = $t.closest('li').prev('li').find('.tag-editor-tag');
+                        if (prev_tag.length) prev_tag.click().find('input').caret(0);
+                        else if ($t.val() && !(o.maxTags && ed.data('tags').length >= o.maxTags)) $(new_tag).insertBefore($t.closest('li')).find('.tag-editor-tag').click();
+                        // allow tabbing to previous element
+                        else {
+                            el.attr('disabled', 'disabled');
+                            setTimeout(function(){ el.removeAttr('disabled'); }, 30);
+                            return;
+                        }
+                        return false;
+                    // tab
+                    } else {
+                        var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                        if (next_tag.length) next_tag.click().find('input').caret(0);
+                        else if ($t.val()) ed.click();
+                        else return; // allow tabbing to next element
+                        return false;
+                    }
+                }
+                // del key
+                else if (e.which == 46 && (!$.trim($t.val()) || ($t.caret() == $t.val().length))) {
+                    var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                    if (next_tag.length) next_tag.click().find('input').caret(0);
+                    else if ($t.val()) ed.click();
+                    return false;
+                }
+                // enter key
+                else if (e.which == 13) {
+                    ed.trigger('click', [$t.closest('li').next('li').find('.tag-editor-tag')]);
+
+                    // trigger blur if maxTags limit is reached
+                    if (o.maxTags && ed.data('tags').length >= o.maxTags) ed.find('input').blur();
+
+                    return false;
+                }
+                // pos1
+                else if (e.which == 36 && !$t.caret()) ed.find('.tag-editor-tag').first().click();
+                // end
+                else if (e.which == 35 && $t.caret() == $t.val().length) ed.find('.tag-editor-tag').last().click();
+                // esc
+                else if (e.which == 27) {
+                    $t.val($t.data('old_tag') ? $t.data('old_tag') : '').blur();
+                    return false;
+                }
+            });
+
+            // create initial tags
+            var tags = o.initialTags.length ? o.initialTags : el.val().split(o.dregex);
+            for (var i=0; i<tags.length; i++) {
+                if (o.maxTags && i >= o.maxTags) break;
+                var tag = $.trim(tags[i].replace(/ +/, ' '));
+                if (tag) {
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    tag_list.push(tag);
+                    ed.append('<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag">'+escape(tag)+'</div><div class="tag-editor-delete"><i></i></div></li>');
+                }
+            }
+            update_globals(true); // true -> no onChange callback
+
+            // init sortable
+            if (o.sortable && $.fn.sortable) ed.sortable({
+                distance: 5, cancel: '.tag-editor-spacer, input', helper: 'clone',
+                update: function(){ update_globals(); }
+            });
+        });
+    };
+
+    $.fn.tagEditor.defaults = {
+        initialTags: [],
+        maxTags: 0,
+        maxLength: 50,
+        delimiter: ',;',
+        placeholder: '',
+        forceLowercase: true,
+        removeDuplicates: true,
+        clickDelete: false,
+        animateDelete: 175,
+        sortable: true, // jQuery UI sortable
+        autocomplete: null, // options dict for jQuery UI autocomplete
+
+        // callbacks
+        onChange: function(){},
+        beforeTagSave: function(){},
+        beforeTagDelete: function(){}
+    };
+}(jQuery));

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,3 @@
+// jQuery tagEditor v1.0.20
+// https://github.com/Pixabay/jQuery-tagEditor
+!function(t){t.fn.tagEditorInput=function(){var e=" ",i=t(this),a=parseInt(i.css("fontSize")),r=t("<span/>").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:i.css("fontSize"),fontFamily:i.css("fontFamily"),fontWeight:i.css("fontWeight"),letterSpacing:i.css("letterSpacing"),whiteSpace:"nowrap"}),l=function(){if(e!==(e=i.val())){r.text(e);var t=r.width()+a;20>t&&(t=20),t!=i.width()&&i.width(t)}};return r.insertAfter(i),i.bind("keyup keydown focus",l)},t.fn.tagEditor=function(e,a,r){function l(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}var n,o=t.extend({},t.fn.tagEditor.defaults,e),c=this;if(o.dregex=new RegExp("["+o.delimiter.replace("-","-")+"]","g"),"string"==typeof e){var s=[];return c.each(function(){var i=t(this),l=i.data("options"),n=i.next(".tag-editor");if("getTags"==e)s.push({field:i[0],editor:n,tags:n.data("tags")});else if("addTag"==e){if(l.maxTags&&n.data("tags").lengt
 h>=l.maxTags)return!1;t('<li><div class="tag-editor-spacer"> '+l.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>').appendTo(n).find(".tag-editor-tag").html('<input type="text" maxlength="'+l.maxLength+'">').addClass("active").find("input").val(a).blur(),r?t(".placeholder",n).remove():n.click()}else"removeTag"==e?(t(".tag-editor-tag",n).filter(function(){return t(this).text()==a}).closest("li").find(".tag-editor-delete").click(),r||n.click()):"destroy"==e&&i.removeClass("tag-editor-hidden-src").removeData("options").off("focus.tag-editor").next(".tag-editor").remove()}),"getTags"==e?s:this}return window.getSelection&&t(document).off("keydown.tag-editor").on("keydown.tag-editor",function(e){if(8==e.which||46==e.which||e.ctrlKey&&88==e.which){try{var a=getSelection(),r="BODY"==document.activeElement.tagName?t(a.getRangeAt(0).startContainer.parentNode).closest(".tag-editor"):0}catch(e){r=0}if(a.rangeCount>0&&r&&r.lengt
 h){var l=[],n=a.toString().split(r.prev().data("options").dregex);for(i=0;i<n.length;i++){var o=t.trim(n[i]);o&&l.push(o)}return t(".tag-editor-tag",r).each(function(){~t.inArray(t(this).text(),l)&&t(this).closest("li").find(".tag-editor-delete").click()}),!1}}}),c.each(function(){function e(){!o.placeholder||c.length||t(".deleted, .placeholder, input",s).length||s.append('<li class="placeholder"><div>'+o.placeholder+"</div></li>")}function i(i){var a=c.toString();c=t(".tag-editor-tag:not(.deleted)",s).map(function(e,i){var a=t.trim(t(this).hasClass("active")?t(this).find("input").val():t(i).text());return a?a:void 0}).get(),s.data("tags",c),r.val(c.join(o.delimiter[0])),i||a!=c.toString()&&o.onChange(r,s,c),e()}function a(e){for(var a,n=e.closest("li"),d=e.val().replace(/ +/," ").split(o.dregex),g=e.data("old_tag"),f=c.slice(0),h=!1,u=0;u<d.length;u++)if(v=t.trim(d[u]).slice(0,o.maxLength),o.forceLowercase&&(v=v.toLowerCase()),a=o.beforeTagSave(r,s,f,g,v),v=a||v,a!==!1&&v&&
 (o.removeDuplicates&&~t.inArray(v,f)&&t(".tag-editor-tag",s).each(function(){t(this).text()==v&&t(this).closest("li").remove()}),f.push(v),n.before('<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag">'+l(v)+'</div><div class="tag-editor-delete"><i></i></div></li>'),o.maxTags&&f.length>=o.maxTags)){h=!0;break}e.attr("maxlength",o.maxLength).removeData("old_tag").val(""),h?e.blur():e.focus(),i()}var r=t(this),c=[],s=t("<ul "+(o.clickDelete?'oncontextmenu="return false;" ':"")+'class="tag-editor"></ul>').insertAfter(r);r.addClass("tag-editor-hidden-src").data("options",o).on("focus.tag-editor",function(){s.click()}),s.append('<li style="width:1px"> </li>');var d='<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>';s.click(function(e,i){var a,r,l=99999;if(!window.getSelection||""==getSelection())return o.maxTags&&s.data("tags").length>=
 o.maxTags?(s.find("input").blur(),!1):(n=!0,t("input:focus",s).blur(),n?(n=!0,t(".placeholder",s).remove(),i&&i.length?r="before":t(".tag-editor-tag",s).each(function(){var n=t(this),o=n.offset(),c=o.left,s=o.top;e.pageY>=s&&e.pageY<=s+n.height()&&(e.pageX<c?(r="before",a=c-e.pageX):(r="after",a=e.pageX-c-n.width()),l>a&&(l=a,i=n))}),"before"==r?t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click():"after"==r?t(d).insertAfter(i.closest("li")).find(".tag-editor-tag").click():t(d).appendTo(s).find(".tag-editor-tag").click(),!1):!1)}),s.on("click",".tag-editor-delete",function(){if(t(this).prev().hasClass("active"))return t(this).closest("li").find("input").caret(-1),!1;var a=t(this).closest("li"),l=a.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,l.text())===!1?!1:(l.addClass("deleted").animate({width:0},o.animateDelete,function(){a.remove(),e()}),i(),!1)}),o.clickDelete&&s.on("mousedown",".tag-editor-tag",function(a){if(a.ctrlKey||a.which>1){var l=t(this)
 .closest("li"),n=l.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,n.text())===!1?!1:(n.addClass("deleted").animate({width:0},o.animateDelete,function(){l.remove(),e()}),i(),!1)}}),s.on("click",".tag-editor-tag",function(e){if(o.clickDelete&&(e.ctrlKey||e.which>1))return!1;if(!t(this).hasClass("active")){var i=t(this).text(),a=Math.abs((t(this).offset().left-e.pageX)/t(this).width()),r=parseInt(i.length*a),n=t(this).html('<input type="text" maxlength="'+o.maxLength+'" value="'+l(i)+'">').addClass("active").find("input");if(n.data("old_tag",i).tagEditorInput().focus().caret(r),o.autocomplete){var c=t.extend({},o.autocomplete),d="select"in c?o.autocomplete.select:"";c.select=function(e,i){d&&d(e,i),setTimeout(function(){s.trigger("click",[t(".active",s).find("input").closest("li").next("li").find(".tag-editor-tag")])},20)},n.autocomplete(c)}}return!1}),s.on("blur","input",function(d){d.stopPropagation();var g=t(this),f=g.data("old_tag"),h=t.trim(g.val().replace(/ +/," "
 ).replace(o.dregex,o.delimiter[0]));if(h){if(h.indexOf(o.delimiter[0])>=0)return void a(g);if(h!=f)if(o.forceLowercase&&(h=h.toLowerCase()),cb_val=o.beforeTagSave(r,s,c,f,h),h=cb_val||h,cb_val===!1){if(f)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}else o.removeDuplicates&&t(".tag-editor-tag:not(.active)",s).each(function(){t(this).text()==h&&t(this).closest("li").remove()})}else{if(f&&o.beforeTagDelete(r,s,c,f)===!1)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}g.parent().html(l(h)).removeClass("active"),h!=f&&i(),e()});var g;s.on("paste","input",function(){t(this).removeAttr("maxlength"),g=t(this),setTimeout(function(){a(g)},30)});var f;s.on("keypress","input",function(e){o.delimiter.indexOf(String.fromCharCode(e.which))>=0&&(f=t(this),setTimeout(function(){a(f)},20))}),s.on("keydown","input",function(e){var i=t(this);if((37==e.which||!o.autocomplete&&38==e.which)&&!i.caret()||8==e.which&&!i.val()
 ){var a=i.closest("li").prev("li").find(".tag-editor-tag");return a.length?a.click().find("input").caret(-1):!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags||t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click(),!1}if((39==e.which||!o.autocomplete&&40==e.which)&&i.caret()==i.val().length){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(9==e.which){if(e.shiftKey){var a=i.closest("li").prev("li").find(".tag-editor-tag");if(a.length)a.click().find("input").caret(0);else{if(!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags)return r.attr("disabled","disabled"),void setTimeout(function(){r.removeAttr("disabled")},30);t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click()}return!1}var l=i.closest("li").next("li").find(".tag-editor-tag");if(l.length)l.click().find("input").caret(0);else{if(!i.val())return;s.click()}return!1}if(!(46!=e.which||t.trim(i.val())&&i.caret(
 )!=i.val().length)){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(13==e.which)return s.trigger("click",[i.closest("li").next("li").find(".tag-editor-tag")]),o.maxTags&&s.data("tags").length>=o.maxTags&&s.find("input").blur(),!1;if(36!=e.which||i.caret()){if(35==e.which&&i.caret()==i.val().length)s.find(".tag-editor-tag").last().click();else if(27==e.which)return i.val(i.data("old_tag")?i.data("old_tag"):"").blur(),!1}else s.find(".tag-editor-tag").first().click()});for(var h=o.initialTags.length?o.initialTags:r.val().split(o.dregex),u=0;u<h.length&&!(o.maxTags&&u>=o.maxTags);u++){var v=t.trim(h[u].replace(/ +/," "));v&&(o.forceLowercase&&(v=v.toLowerCase()),c.push(v),s.append('<li><div class="tag-editor-spacer"> '+o.delimiter[0]+'</div><div class="tag-editor-tag">'+l(v)+'</div><div class="tag-editor-delete"><i></i></div></li>'))}i(!0),o.sortable&&t.fn.sortable&&s.sortable({distance:5,ca
 ncel:".tag-editor-spacer, input",helper:"clone",update:function(){i()}})})},t.fn.tagEditor.defaults={initialTags:[],maxTags:0,maxLength:50,delimiter:",;",placeholder:"",forceLowercase:!0,removeDuplicates:!0,clickDelete:!1,animateDelete:175,sortable:!0,autocomplete:null,onChange:function(){},beforeTagSave:function(){},beforeTagDelete:function(){}}}(jQuery);
\ No newline at end of file

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/readme.md
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/readme.md	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/readme.md	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,119 @@
+jQuery-tagEditor
+================
+
+A powerful and lightweight tag editor plugin for jQuery.
+
+Compatible with jQuery 1.7.0+ in Firefox, Safari, Chrome, Opera, Internet Explorer 8+. IE7 technically works, but no care has gone into CSS/layout bugs.
+Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+This plugin was developed by and for [Pixabay.com](https://pixabay.com/) - an international repository for sharing free public domain images.
+We have implemented this plugin in production and we share this piece of software - in the spirit of Pixabay - freely with others.
+
+## Demo and Documentation
+
+https://goodies.pixabay.com/jquery/tag-editor/demo.html
+
+## Features
+
+* Lightweight: 8.5 kB of JavaScript - less than 3.2 kB gzipped
+* Edit in place tags
+* Intuitive navigation between tags with cursor keys, Tab, Shift+Tab, Enter, Pos1, End, Backspace, Del, and ESC
+* Optional jQuery UI sortable
+* Optional jQuery UI autocomplete
+* Copy-paste or delete multiple selected tags
+* Duplicate tags check
+* Custom delimiter/s
+* Placeholder
+* Custom style for faulty tags
+* Public methods for reading, adding and removing tags + destroy function
+* Callbacks
+* Allows tabindex for form navigation
+* Graceful degradation if JavaScript is disabled
+
+## Changelog
+
+### Version 1.0.20 - 2016/01/30
+
+* Fixed #62: tagEditor is blocking key events on other input and textarea elements on page.
+
+### Version 1.0.19 - 2015/12/02
+
+* Fixed #60: Tag editor fails to handle HTML operator chars.
+
+### Version 1.0.18 - 2015/08/12
+
+* Pull #43: Escape HTML special characters on input.
+
+### Version 1.0.17 - 2015/07/14
+
+* Allow beforeTagSave() to return `false` for discarding certain tag values.
+
+### Version 1.0.16 - 2015/07/01
+
+* Fix #5, #35, #37, #38: "TypeError: owner is null" backspace browser history issue.
+
+### Version 1.0.15 - 2015/05/24
+
+* Fix #31, #33, #34: Added maxTags, removeDuplicates, and animateDelete options.
+
+### Version 1.0.14 - 2015/04/05
+
+* Fix #24: Auto-close tag after selecting autocomplete suggestion by mouse click.
+
+### Version 1.0.13 - 2015/01/26
+
+* Fix #9: Added bower support.
+
+### Version 1.0.12 - 2015/01/16
+
+* Fix #17: Make use of tabindex for form navigation.
+
+### Version 1.0.11 - 2015/01/08
+
+* Use beforeTagSave return value for overwriting new tags.
+
+### Version 1.0.10 - 2015/01/04
+
+* Fix for IE8
+
+### Version 1.0.9 - 2014/12/17
+
+* Optimized internal input autogrow function.
+
+### Version 1.0.8 - 2014/12/14
+
+* Added bower.json file.
+
+### Version 1.0.7 - 2014/11/26
+
+* Removing accursoft's caret plugin (http://code.accursoft.com/caret) from tagEditor source (and adding caret as a dependency).
+
+### Version 1.0.6 - 2014/10/22
+
+* Fixed: Detection for selected field (.tag-editor) on backspace/delete keypress failed in some cases.
+
+### Version 1.0.5 - 2014/09/30
+
+* Merged pull - Added logic for selected field to be .tag-editor only:
+  https://github.com/jegarts/jQuery-tagEditor/commit/498435b562d72c3e502863312b0b2ccbb9e80cab
+
+### Version 1.0.4 - 2014/09/24
+
+* Merged pull stop aco.select form calling itself:
+  https://github.com/jegarts/jQuery-tagEditor/commit/fd0340ba46272290cedc8991f58769945d0fc2c2
+
+### Version 1.0.3 - 2014/09/13
+
+* Removed unnecessary vendor prefixes in CSS stylesheet.
+
+### Version 1.0.2 - 2014/07/16
+
+* Fixed removal of placeholder after calling addTags.
+
+### Version 1.0.1 - 2014/07/16
+
+* Fixed tagEditor for IE8 and IE7. IE7 still has some obvious layout alignment bugs, that can be fixed by conditional CSS rules.
+
+### Version 1.0.0-beta - 2014/07/15
+
+* First release

Added: branches/ScoDoc7/static/libjs/jQuery-tagEditor/tag-editor.jquery.json
===================================================================
--- branches/ScoDoc7/static/libjs/jQuery-tagEditor/tag-editor.jquery.json	                        (rev 0)
+++ branches/ScoDoc7/static/libjs/jQuery-tagEditor/tag-editor.jquery.json	2016-07-14 21:58:11 UTC (rev 1530)
@@ -0,0 +1,31 @@
+{
+	"name": "tag-editor",
+	"title": "tagEditor",
+	"description": "A powerful and lightweight tag editor plugin for jQuery.",
+	"version": "1.0.20",
+	"dependencies": {
+		"jquery": ">=1.7",
+		"caret": ">=1.3.2"
+	},
+	"keywords": [
+		"tags",
+		"keywords",
+		"editor",
+		"ui",
+		"tagging",
+		"jQuery"
+	],
+	"author": {
+		"name": "Simon Steinberger",
+		"url": "https://pixabay.com/users/Simon/",
+		"email": "simon at pixabay.com"
+	},
+	"licenses": [
+		{
+			"type": "MIT",
+			"url": "http://www.opensource.org/licenses/mit-license.php"
+		}
+	],
+	"homepage": "https://goodies.pixabay.com/jquery/tag-editor/demo.html",
+	"demo": "https://goodies.pixabay.com/jquery/tag-editor/demo.html"
+}
\ No newline at end of file


Plus d'informations sur la liste de diffusion Scodoc-devel