witten
/
luminotes
Archived
1
0
Fork 0

Code mostly related to manipulating wiki links, plus a new link pulldown:

* Implemented a new controller.Notebooks.lookup_note_id() method to get only a note's id given its title.
 * Added some new link resolution code to Editor and Wiki, to fill in a link's id according to its destination note.
 * Factored out some of the link finding code into a common Editor.find_link_at_cursor() method.
 * Factored out query parsing into a common parse_query() function, which operates on a link node.
 * Added new Link_pulldown class-thingy to represent the little pulldown you see when the cursor's on a link.
 * Refactored Pulldown's positioning code to support offset positioning (needed for elements within an iframe).
This commit is contained in:
Dan Helfman 2007-08-14 04:13:49 +00:00
parent ec9cd1066c
commit 69caeaf655
6 changed files with 397 additions and 56 deletions

View File

@ -217,6 +217,48 @@ class Notebooks( object ):
note = note,
)
@expose( view = Json )
@strongly_expire
@wait_for_update
@grab_user_id
@async
@update_client
@validate(
notebook_id = Valid_id(),
note_title = Valid_string( min = 1, max = 500 ),
user_id = Valid_id( none_okay = True ),
)
def lookup_note_id( self, notebook_id, note_title, user_id ):
"""
Return a note's id by looking up its title.
@type notebook_id: unicode
@param notebook_id: id of notebook the note is in
@type note_title: unicode
@param note_title: title of the note id to return
@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: { 'note_id': noteid or None }
@raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid
"""
self.check_access( notebook_id, user_id, self.__scheduler.thread )
if not ( yield Scheduler.SLEEP ):
raise Access_error()
self.__database.load( notebook_id, self.__scheduler.thread )
notebook = ( yield Scheduler.SLEEP )
if notebook is None:
note = None
else:
note = notebook.lookup_note_by_title( note_title )
yield dict(
note_id = note and note.object_id or None,
)
@expose( view = Json )
@wait_for_update
@grab_user_id

View File

@ -261,6 +261,44 @@ class Test_notebooks( Test_controller ):
note = result[ "note" ]
assert note == None
def test_lookup_note_id( self ):
self.login()
result = self.http_post( "/notebooks/lookup_note_id/", dict(
notebook_id = self.notebook.object_id,
note_title = self.note.title,
), session_id = self.session_id )
assert result.get( "note_id" ) == self.note.object_id
def test_lookup_note_id_without_login( self ):
result = self.http_post( "/notebooks/lookup_note_id/", dict(
notebook_id = self.notebook.object_id,
note_title = self.note.title,
), session_id = self.session_id )
assert result.get( "error" )
def test_lookup_note_id_with_unknown_notebook( self ):
self.login()
result = self.http_post( "/notebooks/lookup_note_id/", dict(
notebook_id = self.unknown_notebook_id,
note_title = self.note.title,
), session_id = self.session_id )
assert result.get( "error" )
def test_lookup_unknown_note_id( self ):
self.login()
result = self.http_post( "/notebooks/lookup_note_id/", dict(
notebook_id = self.notebook.object_id,
note_title = "unknown title",
), session_id = self.session_id )
assert result.get( "note_id" ) == None
def test_save_note( self, startup = False ):
self.login()

View File

@ -1,5 +1,6 @@
body {
padding: 1em;
font-size: 100%;
line-height: 140%;
}

View File

@ -300,3 +300,15 @@ ol li {
margin-left: 2em;
margin-right: 2em;
}
.field_label {
font-weight: bold;
}
.text_field {
margin-right: 0.5em;
padding: 0.25em;
border: #999999 1px solid;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
}

View File

@ -179,7 +179,6 @@ Editor.prototype.finish_init = function () {
this.scrape_title();
if ( this.init_focus )
this.focus();
signal( this, "state_changed", this );
}
Editor.prototype.highlight = function ( scroll ) {
@ -247,6 +246,24 @@ Editor.prototype.resize = function () {
setElementDimensions( this.iframe, dimensions );
}
Editor.prototype.resolve_link = function ( link ) {
// in case the link is to ourself, first grab the most recent version of our title
this.scrape_title();
var id;
var link_title = scrapeText( link );
var editor = note_titles[ link_title ];
// if the link's title corresponds to an open note id, set that as the link's destination
if ( editor ) {
id = editor.id;
link.href = "/notebooks/" + this.notebook_id + "?note_id=" + id;
// otherwise, resolve the link by looking up the link's title on the server
} else {
signal( this, "resolve_link", link_title, link );
return;
}
}
Editor.prototype.key_pressed = function ( event ) {
signal( this, "key_pressed", this, event );
@ -287,34 +304,17 @@ Editor.prototype.mouse_clicked = function ( event ) {
event.stop();
// in case the link is to ourself, first grab the most recent version of our title
this.scrape_title();
var id;
var link_title = scrapeText( link );
var editor = note_titles[ link_title ];
var href_leaf = link.href.split( "?note_id=" ).pop();
// if the link's title corresponds to an open note id, set that as the link's destination
if ( editor ) {
id = editor.id;
link.href = "/notebooks/" + this.notebook_id + "?note_id=" + id;
// if this is a new link, get a new note id and set it for the link's destination
} else if ( href_leaf == "new" ) {
signal( this, "load_editor_by_title", link_title, this.iframe.id );
return;
// otherwise, use the id from link's current destination
} else {
// the last part of the current link's href is the note id
id = href_leaf;
}
// find the note corresponding to the linked id, or create a new note
// if the note corresponding to the linked id is already open, highlight it
var query = parse_query( link );
var link_title = query.title || scrapeText( link );
var id = query.note_id;
var iframe = getElement( "note_" + id );
if ( iframe ) {
iframe.editor.highlight();
return;
}
// otherwise, load the note for that id
signal( this, "load_editor", link_title, this.iframe.id, id );
}
@ -385,24 +385,22 @@ Editor.prototype.start_link = function () {
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
var links = getElementsByTagAndClassName( "a", null, parent = this.document );
for ( var i in links ) {
var link = links[ i ];
var link_title = scrapeText( link );
var char_code = link_title.charCodeAt( 0 );
// look for links titled with a space or nbsp character
if ( link_title.length == 1 && char_code == 0x20 || char_code == 0xa0 ) {
for ( var j in link.childNodes ) {
var child = link.childNodes[ j ];
if ( child.nodeType == 3 ) // type of text node
child.nodeValue = "";
}
selection.collapse( link, 0 );
// nuke the link title and collapse the selection, yielding a tasty new link that's completely
// titleless and unselected
var link = this.find_link_at_cursor();
if ( link ) {
for ( var j in link.childNodes ) {
var child = link.childNodes[ j ];
if ( child.nodeType == 3 ) // type of text node
child.nodeValue = "";
}
selection.collapse( link, 0 );
}
// otherwise, just create a link with the selected text as the link title
} else {
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
var link = this.find_link_at_cursor();
signal( this, "resolve_link", scrapeText( link ), link );
}
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
@ -413,13 +411,18 @@ Editor.prototype.start_link = function () {
range.text = " ";
range.moveStart( "character", -1 );
range.select();
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
} else {
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
var link = this.find_link_at_cursor();
signal( this, "resolve_link", scrapeText( link ), link );
}
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
}
}
Editor.prototype.end_link = function () {
var link = this.find_link_at_cursor();
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
this.exec_command( "unlink" );
} else if ( this.document.selection ) { // browsers such as IE
@ -439,6 +442,49 @@ Editor.prototype.end_link = function () {
range.select();
range.pasteHTML( "" );
}
var query = parse_query( link );
var link_title = query.title || scrapeText( link );
signal( this, "resolve_link", link_title, link );
}
Editor.prototype.find_link_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
var link = selection.anchorNode;
while ( link.nodeName != "A" ) {
link = link.parentNode;
if ( !link )
break;
}
if ( link ) return link;
// well, that didn't work, so try the selection's focus node instead
link = selection.focusNode;
while ( link.nodeName != "A" ) {
link = link.parentNode;
if ( !link )
return null;
}
return link;
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
var link = range.parentElement();
while ( link.nodeName != "A" ) {
link = link.parentNode;
if ( !link )
return null;
}
return link;
}
return null;
}
Editor.prototype.focus = function () {
@ -514,3 +560,9 @@ Editor.prototype.shutdown = function( event ) {
} catch ( e ) { }
} } );
}
// convenience function for parsing a link that has an href URL containing a query string
function parse_query( link ) {
return parseQueryString( link.href.split( "?" ).pop() );
}

View File

@ -6,6 +6,7 @@ function Wiki() {
this.notebook_id = getElement( "notebook_id" ).value;
this.read_write = false;
this.startup_notes = new Array(); // map of startup notes: note id to bool
this.link_pulldowns = new Array(); // map of link pulldowns: link object to pulldown
this.invoker = new Invoker();
connect( this.invoker, "error_message", this, "display_error" );
@ -176,6 +177,19 @@ Wiki.prototype.create_blank_editor = function ( event ) {
Wiki.prototype.load_editor = function ( note_title, from_iframe_id, note_id, revision ) {
var self = this;
// if there's not a valid destination note id, then load by title instead of by id
if ( note_id == "new" || note_id == "null" ) {
this.invoker.invoke(
"/notebooks/load_note_by_title", "GET", {
"notebook_id": this.notebook_id,
"note_title": note_title,
"revision": revision
},
function ( result ) { self.parse_loaded_editor( result, from_iframe_id, note_title, revision ); }
);
return;
}
this.invoker.invoke(
"/notebooks/load_note", "GET", {
"notebook_id": this.notebook_id,
@ -186,15 +200,27 @@ Wiki.prototype.load_editor = function ( note_title, from_iframe_id, note_id, rev
);
}
Wiki.prototype.load_editor_by_title = function ( note_title, from_iframe_id ) {
var self = this;
Wiki.prototype.resolve_link = function ( note_title, link, force ) {
// if the link already has an id and the force flag isn't set, then the link is already resolved,
// so we can just bail
if ( link.href ) {
var id = parse_query( link ).note_id;
if ( id != "new" && id != "null" && !force )
return;
}
if ( note_title.length == 0 )
return;
var self = this;
this.invoker.invoke(
"/notebooks/load_note_by_title", "GET", {
"/notebooks/lookup_note_id", "GET", {
"notebook_id": this.notebook_id,
"note_title": note_title
},
function ( result ) { self.parse_loaded_editor( result, from_iframe_id, note_title ); }
function ( result ) {
link.href = "/notebooks/" + self.notebook_id + "?note_id=" + result.note_id;
}
);
}
@ -222,7 +248,6 @@ Wiki.prototype.parse_loaded_editor = function ( result, from_iframe_id, note_tit
Wiki.prototype.create_editor = function ( id, note_text, deleted_from, revisions_list, from_iframe_id, note_title, read_write, highlight, focus ) {
this.clear_messages();
this.clear_pulldowns();
var self = this;
if ( isUndefinedOrNull( id ) ) {
@ -266,7 +291,7 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from, revisions
}
connect( editor, "load_editor", this, "load_editor" );
connect( editor, "load_editor_by_title", this, "load_editor_by_title" );
connect( editor, "resolve_link", this, "resolve_link" );
connect( editor, "hide_clicked", function ( event ) { self.hide_editor( event, editor ) } );
connect( editor, "submit_form", function ( url, form ) {
self.invoker.invoke( url, "POST", null, null, form );
@ -277,16 +302,38 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from, revisions
Wiki.prototype.editor_state_changed = function ( editor ) {
this.update_toolbar();
this.display_link_pulldown( editor );
}
Wiki.prototype.display_link_pulldown = function ( editor ) {
var link = editor.find_link_at_cursor();
if ( !link ) {
this.clear_pulldowns();
return;
}
var pulldown = this.link_pulldowns[ link ];
if ( pulldown )
pulldown.update_position();
// if the cursor is now on a link, display a link pulldown if there isn't already one open
if ( hasElementClass( "createLink", "button_down" ) ) {
if ( !pulldown ) {
this.clear_pulldowns();
new Link_pulldown( this, this.notebook_id, this.invoker, editor, link );
}
}
}
Wiki.prototype.editor_focused = function ( editor, fire_and_forget ) {
this.clear_messages();
this.clear_pulldowns();
if ( editor )
addElementClass( editor.iframe, "focused_note_frame" );
if ( this.focused_editor && this.focused_editor != editor ) {
this.clear_pulldowns();
removeElementClass( this.focused_editor.iframe, "focused_note_frame" );
// if the formerly focused editor is completely empty, then remove it as the user leaves it and switches to this editor
@ -388,6 +435,8 @@ Wiki.prototype.toggle_link_button = function ( event ) {
this.focused_editor.start_link();
else
this.focused_editor.end_link();
this.display_link_pulldown( this.focused_editor );
}
event.stop();
@ -666,26 +715,50 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) {
connect( window, "onload", function ( event ) { new Wiki(); } );
function Pulldown( wiki, notebook_id, pulldown_id, button ) {
function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to ) {
this.wiki = wiki;
this.notebook_id = notebook_id;
this.div = createDOM( "div", { "id": pulldown_id, "class": "pulldown" } );
this.div.pulldown = this;
this.anchor = anchor;
this.relative_to = relative_to;
addElementClass( this.div, "invisible" );
appendChildNodes( document.body, this.div );
var self = this;
// position the pulldown under the button that opened it
var position = getElementPosition( button );
var button_dimensions = getElementDimensions( button );
var div_dimensions = getElementDimensions( this.div );
position.y += button_dimensions.h;
var position = calculate_position( anchor, relative_to );
setElementPosition( this.div, position );
removeElementClass( this.div, "invisible" );
}
}
function calculate_position( anchor, relative_to ) {
// position the pulldown under the anchor
var position = getElementPosition( anchor );
if ( relative_to ) {
var relative_pos = getElementPosition( relative_to );
if ( relative_pos ) {
position.x += relative_pos.x;
position.y += relative_pos.y;
}
}
var anchor_dimensions = getElementDimensions( anchor );
// if the anchor has no height, move the position down a bit an arbitrary amount
if ( anchor_dimensions.h == 0 )
position.y += 8;
else
position.y += anchor_dimensions.h + 4;
return position;
}
Pulldown.prototype.update_position = function () {
var position = calculate_position( this.anchor, this.relative_to );
setElementPosition( this.div, position );
}
Pulldown.prototype.shutdown = function () {
removeElement( this.div );
@ -773,9 +846,132 @@ Changes_pulldown.prototype.link_clicked = function( event, note_id ) {
event.stop();
}
Options_pulldown.prototype.shutdown = function () {
Changes_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this );
for ( var i in this.links )
disconnectAll( this.links[ i ] );
}
function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
wiki.link_pulldowns[ link ] = this;
this.link = link;
Pulldown.call( this, wiki, notebook_id, "link_" + editor.id, link, editor.iframe );
this.invoker = invoker;
this.editor = editor;
this.title_field = createDOM( "input", { "class": "text_field", "size": "25", "maxlength": "256" } );
this.note_preview = createDOM( "span", {} );
this.previous_title = "";
var self = this;
connect( this.title_field, "onclick", function ( event ) { self.title_field_clicked( event ); } );
connect( this.title_field, "onchange", function ( event ) { self.title_field_changed( event ); } );
connect( this.title_field, "onblur", function ( event ) { self.title_field_changed( event ); } );
connect( this.title_field, "onkeydown", function ( event ) { self.title_field_key_pressed( event ); } );
appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "links to: " ) );
appendChildNodes( this.div, this.title_field );
appendChildNodes( this.div, this.note_preview );
var query = parse_query( link );
var link_title = query.title || scrapeText( link );
var id = query.note_id;
if ( id == "new" || id == "null" ) {
this.title_field.value = link_title;
replaceChildNodes( self.note_preview, "empty note" );
return;
}
// if this link has an actual destination note id set, then load that note, displaying its title
// and a preview of its contents
this.invoker.invoke(
"/notebooks/load_note", "GET", {
"notebook_id": this.notebook_id,
"note_id": id
},
function ( result ) {
if ( result.note ) {
self.title_field.value = result.note.title;
self.display_preview( result.note.title, result.note.contents );
} else {
self.title_field.value = link_title;
replaceChildNodes( self.note_preview, "empty note" );
}
}
);
}
Link_pulldown.prototype = Pulldown;
Link_pulldown.prototype.constructor = Link_pulldown;
Link_pulldown.prototype.display_preview = function ( title, contents ) {
var contents_node = createDOM( "span", {} );
contents_node.innerHTML = contents;
var contents = scrapeText( contents_node );
// remove the title from the scraped contents text
if ( contents.indexOf( title ) == 0 )
contents = contents.substr( title.length );
if ( contents.length == 0 ) {
replaceChildNodes( this.note_preview, "empty note" );
} else {
var max_preview_length = 40;
var preview = contents.substr( 0, max_preview_length ) + ( ( contents.length > max_preview_length ) ? "..." : "" );
replaceChildNodes( this.note_preview, preview );
}
}
Link_pulldown.prototype.title_field_clicked = function ( event ) {
event.stop();
}
Link_pulldown.prototype.title_field_changed = function ( event ) {
// if the title is actually unchanged, then bail
if ( this.title_field.value == this.previous_title )
return;
var self = this;
replaceChildNodes( this.note_preview, "" );
this.previous_title = this.title_field.value;
this.invoker.invoke(
"/notebooks/load_note_by_title", "GET", {
"notebook_id": this.notebook_id,
"note_title": this.title_field.value
},
function ( result ) {
if ( result.note ) {
self.link.href = "/notebooks/" + self.notebook_id + "?note_id=" + result.note.object_id;
self.display_preview( result.note.title, result.note.contents );
} else {
self.link.href = "/notebooks/" + self.notebook_id + "?title=" + self.title_field.value + "&note_id=null";
replaceChildNodes( self.note_preview, "empty note" );
}
}
);
}
Link_pulldown.prototype.title_field_key_pressed = function ( event ) {
// if enter is pressed, consider the title field altered. this is necessary because IE neglects
// to issue an onchange event when enter is pressed in an input field
if ( event.key().code == 13 ) {
this.title_field_changed();
event.stop();
}
}
Link_pulldown.prototype.update_position = function ( anchor, relative_to ) {
Pulldown.prototype.update_position.call( this, anchor, relative_to );
}
Link_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this );
disconnectAll( this.title_field );
if ( this.link )
delete this.wiki.link_pulldowns[ this.link ];
}