From eaaf1b3de5c0a191084aebad208bcfe463bc1a3b Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 12 Apr 2008 00:51:32 +0000 Subject: [PATCH] Lots more work on the note tree control. Still not done, and some of the new icons suck. --- controller/Notebooks.py | 65 +++++++++++++++++++++++++++++ static/css/style.css | 59 ++++++++++++++++++++++++++- static/images/file_icon.png | Bin 0 -> 458 bytes static/images/loading.gif | Bin 0 -> 673 bytes static/images/note_icon.png | Bin 0 -> 394 bytes static/images/web_icon.png | Bin 0 -> 929 bytes static/js/Wiki.js | 79 +++++++++++++++++++++++++++++------- view/Note_tree_area.py | 56 +++++++++++++++++-------- 8 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 static/images/file_icon.png create mode 100644 static/images/loading.gif create mode 100644 static/images/note_icon.png create mode 100644 static/images/web_icon.png 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 0000000000000000000000000000000000000000..168d472f34f5b40975a5db5d7b55d8849c6534de GIT binary patch literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G^tAk28_ZrvZCAbW|YuPggCMnMrZ?UmNHAY)`pTq8=H^K)}k^GX<;i&7IyQd1Pl zGfOfQLNZbn+&z5*-lwSMF)%RddAc};Se$-3$={pFQKDU5aoU+JYd<{M8xa(EP+?ce z3je?h=^YMD=e$n`e35LjV!fz3LFCf|-4zDjuciqsEEi8qxX(9xVXoS|-v#fRMf2$p4oLFYx|8`cbPXEPMp;Yl`;@M{zX7>$vnj=cu9(}#~ zzTwv8*aHWmd<5t52=^abhl8d9(}|#0;LUelF{r5}E*{D8m#0 literal 0 HcmV?d00001 diff --git a/static/images/loading.gif b/static/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..27b0a4c71736b4e81368cf00f0afc89217864b2f GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q>(?&^28Mn6_Bl8>02v7h3H9~${r&xl|D>FYQWHy3QxwWG zOEMG^vl1(E@)J|^GV{{%85DoAFmM5tEB@#9a}5c0b_{Se(lcOY1PbW@G0D8JzOW7Eb1{;*gxf?l&Wi({-cap7;r7l!E}X+)Eqz!)jo2f+oR?VMR6u=p2PZ6D-9amptEdYo3ARahV5- zdQRKso(f;H$dHGPTckK~_XgANQyCi#HgI|}DR^^EXzVP; zcee@)rY3aUO`LPrWLm4E+ff6N2??{eG4iq;auAqp*l_6Dxrs1i*fER&#{BA3rz>+b wugh_899HN%UdYE{Xt46shIIiFhk(%)c55k@bl z6ot&>{5&pOIN;*aNY+#^GB7Y!@Jh@sO;S+L%FNA8OjcJ2E=kSIOD)n-;L0c|DX`Ml zhe+t+WkQD5@^|NrF^7k;-=R(_o5_N2#g)7ze&DbKwa4n9^fesU-}8|V}UPgg&ebxsLQ E0E4@K0ssI2 literal 0 HcmV?d00001 diff --git a/static/images/web_icon.png b/static/images/web_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0cd089be3f04edd25bea63ae78a4eb908d2fd16 GIT binary patch literal 929 zcmV;S177@zP)W)H3f=_4qr(0N z4Wb9}B=+RVf{>!!OQD4N(%7^fdMIt&glS$#_9dH_O?GB>=6Rm+A`-B_^Ow)b%TVxPCJ|+)FTY!Z=u2( zKbEup;)?mr;?G}{{sRid>4`Uv@6SxUQ%mdzJ(#A+#`Xo`@jl{~Pt2^4%Pi3M%q*TS zxcbv2zp`vhI_1)=7@EA2v*Rp&{_*vmbjnAVc^0c5;f1G2CF3OG7Q@edPgn9Dz8~WI z0j>RRu3WuX4+4F}04|?D?Wav+k4~84&ZEz$?~bChCJYspxkTIR<=(=ltklN|WrDa> z7f3luFO#F?Szoy6+qd8)g|al>wswxVN5qA22!<#4#~CfP)mtyY#!m_kV?46NBO z{H6ioY%<~Tq;`zz_7Fj+=!6o#6XNF``XMMy?CNG-#=Dw4U3T% zXE^r4Ja(+bdb2%M(niTrPkQ-fO!MB{&;D_%O+zya3)boBc-I; z%+vNHuGhi!0=D;T!cgILY)m35%k@3rhT)V;Eh*K9bAK(gGpS3soi2hPV*b(VwEcj# z7tr]+\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, + )