diff --git a/controller/Notebooks.py b/controller/Notebooks.py
index cc6a5f3..004fec5 100644
--- a/controller/Notebooks.py
+++ b/controller/Notebooks.py
@@ -1,4 +1,5 @@
import re
+import cgi
import cherrypy
from datetime import datetime
from Expose import expose
@@ -15,6 +16,7 @@ from model.User_revision import User_revision
from view.Main_page import Main_page
from view.Json import Json
from view.Html_file import Html_file
+from view.Note_tree_area import Note_tree_area
class Access_error( Exception ):
@@ -33,6 +35,9 @@ class Access_error( Exception ):
class Notebooks( object ):
WHITESPACE_PATTERN = re.compile( u"\s+" )
+ LINK_PATTERN = re.compile( u']+\s)?href="([^"]+)"(?:\s+target="([^"]*)")?[^>]*)>([^<]+)', re.IGNORECASE )
+ FILE_PATTERN = re.compile( u'/files/' )
+
"""
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
"""
@@ -449,6 +454,66 @@ class Notebooks( object ):
revisions = revisions,
)
+ @expose( view = Json )
+ @strongly_expire
+ @end_transaction
+ @grab_user_id
+ @validate(
+ notebook_id = Valid_id(),
+ note_id = Valid_id(),
+ user_id = Valid_id( none_okay = True ),
+ )
+ def load_note_links( self, notebook_id, note_id, user_id = None ):
+ """
+ Return a list of HTTP links found within the contents of the given note.
+
+ @type notebook_id: unicode
+ @param notebook_id: id of notebook the note is in
+ @type note_id: unicode
+ @param note_id: id of note in question
+ @type user_id: unicode or NoneType
+ @param user_id: id of current logged-in user (if any), determined by @grab_user_id
+ @rtype: json dict
+ @return: { 'tree_html': html_fragment }
+ @raise Access_error: the current user doesn't have access to the given notebook or note
+ @raise Validation_error: one of the arguments is invalid
+ """
+ if not self.__users.check_access( user_id, notebook_id ):
+ raise Access_error()
+
+ note = self.__database.load( Note, note_id )
+ items = []
+
+ for match in self.LINK_PATTERN.finditer( note.contents ):
+ ( attributes, href, target, title ) = match.groups()
+
+ # if it has a link target, it's a link to an external web site
+ if target:
+ items.append( Note_tree_area.make_item( title, attributes, "note_tree_external_link" ) )
+ continue
+
+ # if it has '/files/' in its path, it's an uploaded file link
+ if self.FILE_PATTERN.search( href ):
+ items.append( Note_tree_area.make_item( title, attributes, "note_tree_file_link" ) )
+ continue
+
+ # if it has a note_id, load that child note and see whether it has any children of its own
+ child_note_ids = cgi.parse_qs( href.split( '?' )[ -1 ] ).get( u"note_id" )
+
+ if child_note_ids:
+ child_note_id = child_note_ids[ 0 ]
+ child_note = self.__database.load( Note, child_note_id )
+ if child_note and self.LINK_PATTERN.search( child_note.contents ):
+ items.append( Note_tree_area.make_item( title, attributes, "note_tree_link", has_children = True ) )
+ continue
+
+ # otherwise, it's childless
+ items.append( Note_tree_area.make_item( title, attributes, "note_tree_link", has_children = False ) )
+
+ return dict(
+ tree_html = unicode( Note_tree_area.make_tree( items ) ),
+ )
+
@expose( view = Json )
@end_transaction
@grab_user_id
diff --git a/static/css/style.css b/static/css/style.css
index c48109c..c50646e 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -230,6 +230,24 @@ img {
background-image: url(/static/images/arrow_down_hover.png);
}
+#tree_arrow_hover_preload {
+ height: 0;
+ overflow: hidden;
+ background-image: url(/static/images/tree_arrow_hover.png);
+}
+
+#tree_arrow_down_preload {
+ height: 0;
+ overflow: hidden;
+ background-image: url(/static/images/tree_arrow_down.png);
+}
+
+#tree_arrow_down_hover_preload {
+ height: 0;
+ overflow: hidden;
+ background-image: url(/static/images/tree_arrow_down_hover.png);
+}
+
#note_tree_area {
position: fixed;
width: 17em;
@@ -246,6 +264,8 @@ img {
overflow-y: auto;
padding-left: 1em;
margin-right: 1em;
+ border-collapse: collapse;
+ margin-left: 0.5em;
}
#note_tree_area_title {
@@ -253,6 +273,26 @@ img {
margin-bottom: 0.25em;
}
+.note_tree_link {
+ background: url(/static/images/note_icon.png) left center no-repeat;
+ padding-left: 18px;
+}
+
+.note_tree_external_link {
+ background: url(/static/images/web_icon.png) left center no-repeat;
+ padding-left: 18px;
+}
+
+.note_tree_file_link {
+ background: url(/static/images/file_icon.png) left center no-repeat;
+ padding-left: 18px;
+}
+
+.note_tree_loading {
+ background: url(/static/images/loading.gif) left center no-repeat;
+ padding-left: 20px;
+}
+
#link_area {
float: right;
text-align: left;
@@ -535,7 +575,8 @@ img {
.tree_expander {
float: left;
- width: 20px;
+ width: 11px;
+ margin-right: 4px;
height: 1.5em;
background: url(/static/images/tree_arrow.png) no-repeat center center;
}
@@ -545,9 +586,23 @@ img {
cursor: pointer;
}
+.tree_expander_expanded {
+ float: left;
+ width: 11px;
+ margin-right: 4px;
+ height: 1.5em;
+ background: url(/static/images/tree_arrow_down.png) no-repeat center center;
+}
+
+.tree_expander_expanded:hover {
+ background: url(/static/images/tree_arrow_down_hover.png) no-repeat center center;
+ cursor: pointer;
+}
+
.tree_expander_empty {
float: left;
- width: 20px;
+ width: 11px;
+ margin-right: 4px;
height: 1.5em;
}
diff --git a/static/images/file_icon.png b/static/images/file_icon.png
new file mode 100644
index 0000000..168d472
Binary files /dev/null and b/static/images/file_icon.png differ
diff --git a/static/images/loading.gif b/static/images/loading.gif
new file mode 100644
index 0000000..27b0a4c
Binary files /dev/null and b/static/images/loading.gif differ
diff --git a/static/images/note_icon.png b/static/images/note_icon.png
new file mode 100644
index 0000000..d3b4ad5
Binary files /dev/null and b/static/images/note_icon.png differ
diff --git a/static/images/web_icon.png b/static/images/web_icon.png
new file mode 100644
index 0000000..c0cd089
Binary files /dev/null and b/static/images/web_icon.png differ
diff --git a/static/js/Wiki.js b/static/js/Wiki.js
index e2faf18..8dd4545 100644
--- a/static/js/Wiki.js
+++ b/static/js/Wiki.js
@@ -2563,15 +2563,24 @@ function Note_tree( wiki, notebook_id, invoker ) {
var links = getElementsByTagAndClassName( "a", "note_tree_link", "note_tree_area" );
var self = this;
+ function connect_expander( note_id ) {
+ connect( "note_tree_expander_" + note_id, "onclick", function ( event ) { self.expand_link( event, note_id ); } );
+ }
+
for ( var i in links ) {
var link = links[ i ];
- // TODO: connect expander as well
+ var query = parse_query( link );
+ var note_id = query[ "note_id" ];
+
+ if ( note_id )
+ connect_expander( note_id );
+
connect( link, "onclick", function ( event ) { self.link_clicked( event ); } );
}
// connect to the wiki note events
connect( wiki, "note_renamed", function ( editor, new_title ) { self.rename_link( editor, new_title ); } );
- connect( wiki, "note_added", function ( editor ) { self.add_link( editor ); } );
+ connect( wiki, "note_added", function ( editor ) { self.add_root_link( editor ); } );
connect( wiki, "note_removed", function ( id ) { self.remove_link( id ); } );
connect( wiki, "note_saved", function ( editor ) { self.update_link( editor ); } );
}
@@ -2590,16 +2599,16 @@ Note_tree.prototype.link_clicked = function ( event ) {
LINK_PATTERN = /]+\s)?href="[^"]+"[^>]*>/;
-Note_tree.prototype.add_link = function ( editor ) {
+Note_tree.prototype.add_root_link = function ( editor ) {
// for now, only add startup notes to the note tree
if ( !editor.startup )
return;
// display the tree expander arrow if the given note's editor contains any outgoing links
if ( LINK_PATTERN.exec( editor.contents() ) )
- var expander = createDOM( "div", { "class": "tree_expander" } );
+ var expander = createDOM( "td", { "class": "tree_expander", "id": "note_tree_expander_" + editor.id } );
else
- var expander = createDOM( "div", { "class": "tree_expander_empty" } );
+ var expander = createDOM( "td", { "class": "tree_expander_empty", "id": "note_tree_expander_" + editor.id } );
var link = createDOM( "a", {
"href": "/notebooks/" + this.notebook_id + "?note_id=" + editor.id,
@@ -2608,26 +2617,26 @@ Note_tree.prototype.add_link = function ( editor ) {
}, editor.title || "untitled note" );
appendChildNodes( "note_tree_area_holder", createDOM(
- "div",
+ "tr",
{ "id": "note_tree_item_" + editor.id, "class": "note_tree_item" },
expander,
- link
+ createDOM( "td", {}, link )
) );
var self = this;
- // TODO: connect expander as well
+ connect( expander, "onclick", function ( event ) { self.expand_link( event, editor.id ); } );
connect( link, "onclick", function ( event ) { self.link_clicked( event ); } );
}
-Note_tree.prototype.remove_link = function ( id ) {
- removeElement( "note_tree_item_" + id );
+Note_tree.prototype.remove_link = function ( note_id ) {
+ removeElement( "note_tree_item_" + note_id );
}
Note_tree.prototype.rename_link = function ( editor, new_title ) {
var link = getElement( "note_tree_link_" + editor.id );
if ( !link ) {
- this.add_link( editor );
+ this.add_root_link( editor );
return;
}
@@ -2638,7 +2647,7 @@ Note_tree.prototype.update_link = function ( editor ) {
var link = getElement( "note_tree_link_" + editor.id );
if ( !link ) {
- this.add_link( editor );
+ this.add_root_link( editor );
return;
}
@@ -2646,10 +2655,52 @@ Note_tree.prototype.update_link = function ( editor ) {
this.remove_link( editor.id );
// TODO: if link is expanded, update child links (if any)
+ // TODO: hide/show the link's expander arrow based on the precense of outgoing links
}
-Note_tree.prototype.expand_link = function ( id ) {
+Note_tree.prototype.expand_link = function ( event, note_id ) {
+ // FIXME: use of note_id here is problematic. if a given note is listed in multiple different locations, the id won't be unique in
+ // the DOM
+ var expander = event.target();
+
+ if ( !expander || hasElementClass( expander, "tree_expander_empty" ) )
+ return;
+
+ // if it's collapsed, expand it
+ if ( hasElementClass( expander, "tree_expander" ) ) {
+ var children_area = createDOM( "div", { "id": "note_tree_children_" + note_id },
+ createDOM( "span", { "class": "note_tree_loading" }, "loading..." )
+ );
+
+ swapElementClass( expander, "tree_expander", "tree_expander_expanded" );
+ insertSiblingNodesAfter( "note_tree_link_" + note_id,
+ children_area
+ );
+
+ var self = this;
+ this.invoker.invoke(
+ "/notebooks/load_note_links", "GET", {
+ "notebook_id": this.notebook_id,
+ "note_id": note_id
+ },
+ function ( result ) {
+ var span = createDOM( "span" );
+ span.innerHTML = result.tree_html;
+ replaceChildNodes( children_area, span );
+ }
+ );
+
+ return;
+ }
+
+ // if it's expanded, collapse it
+ if ( hasElementClass( expander, "tree_expander_expanded" ) ) {
+ swapElementClass( expander, "tree_expander_expanded", "tree_expander" );
+ var children = getElement( "note_tree_children_" + note_id );
+ if ( children )
+ removeElement( children );
+ }
}
-Note_tree.prototype.collapse_link = function ( id ) {
+Note_tree.prototype.collapse_link = function ( event, note_id ) {
}
diff --git a/view/Note_tree_area.py b/view/Note_tree_area.py
index edeaae6..7c8cdab 100644
--- a/view/Note_tree_area.py
+++ b/view/Note_tree_area.py
@@ -1,9 +1,9 @@
import re
-from Tags import Div, Span, H4, A
+from Tags import Div, Span, H4, A, Table, Tr, Td
class Note_tree_area( Div ):
- LINK_PATTERN = re.compile( u']+\s)?href="[^"]+"[^>]*>', re.IGNORECASE )
+ LINK_PATTERN = re.compile( u']+\s)?href="[^"]+"[^>]*>', re.IGNORECASE )
def __init__( self, toolbar, notebook, root_notes, total_notes_count ):
Div.__init__(
@@ -17,20 +17,44 @@ class Note_tree_area( Div ):
),
id = u"note_tree_area_title",
),
- [ Div(
- self.LINK_PATTERN.search( note.contents ) and \
- Div( id = u"note_tree_expander_" + note.object_id, class_ = u"tree_expander" ) or
- Div( id = u"note_tree_expander_" + note.object_id, class_ = u"tree_expander_empty" ),
- A(
- note.title or u"untitled note",
- href = u"/notebooks/%s?note_id=%s" % ( notebook.object_id, note.object_id ),
- id = u"note_tree_link_" + note.object_id,
- class_ = u"note_tree_link",
- ),
- id = u"note_tree_item_" + note.object_id,
- class_ = u"note_tree_item",
- ) for note in root_notes ],
- id = u"note_tree_area_holder",
+ self.make_tree(
+ [ self.make_item(
+ title = note.title,
+ link_attributes = u"href=/notebooks/%s?note_id=%s" % ( notebook.object_id, note.object_id ),
+ link_class = u"note_tree_link",
+ has_children = self.LINK_PATTERN.search( note.contents ),
+ root_note_id = note.object_id,
+ ) for note in root_notes ],
+ tree_id = u"note_tree_area_holder",
+ ),
),
+ Span( id = "tree_arrow_hover_preload" ),
+ Span( id = "tree_arrow_down_preload" ),
+ Span( id = "tree_arrow_down_hover_preload" ),
id = u"note_tree_area",
)
+
+ @staticmethod
+ def make_item( title, link_attributes, link_class, has_children = False, root_note_id = None ):
+ return Tr(
+ has_children and \
+ Td( id = root_note_id and u"note_tree_expander_" + root_note_id or None, class_ = u"tree_expander" ) or
+ Td( id = root_note_id and u"note_tree_expander_" + root_note_id or None, class_ = u"tree_expander_empty" ),
+ Td(
+ u"%s" % (
+ link_attributes,
+ root_note_id and u" id=note_tree_link_" + root_note_id or None,
+ link_class,
+ title or u"untitled note",
+ ),
+ ),
+ id = root_note_id and u"note_tree_item_" + root_note_id or None,
+ class_ = u"note_tree_item",
+ )
+
+ @staticmethod
+ def make_tree( items, tree_id = None ):
+ return Table(
+ items,
+ id = tree_id,
+ )