witten
/
luminotes
Archived
1
0
Fork 0

First pass for suggest-as-you-type for linking.

This commit is contained in:
Dan Helfman 2008-06-27 16:11:09 -07:00
parent 599971ba01
commit fe139cc749
5 changed files with 294 additions and 8 deletions

View File

@ -904,6 +904,52 @@ class Notebooks( object ):
storage_bytes = user.storage_bytes, storage_bytes = user.storage_bytes,
) )
@expose( view = Json )
@strongly_expire
@end_transaction
@grab_user_id
@validate(
notebook_id = Valid_id(),
search_text = unicode,
user_id = Valid_id( none_okay = True ),
)
def search_titles( self, notebook_id, search_text, user_id ):
"""
Search the note titles within the given notebook for the given search text, and return matching
notes. The search is case-insensitive. The returned notes include title summaries with the
search term highlighted and are ordered by descending revision timestamp.
@type notebook_id: unicode
@param notebook_id: id of notebook to search
@type search_text: unicode
@param search_text: search term
@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: { 'notes': [ matching notes ] }
@raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid
@raise Search_error: the provided search_text is invalid
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
MAX_SEARCH_TEXT_LENGTH = 256
if len( search_text ) > MAX_SEARCH_TEXT_LENGTH:
raise Validation_error( u"search_text", None, unicode, message = u"is too long" )
if len( search_text ) == 0:
raise Validation_error( u"search_text", None, unicode, message = u"is missing" )
notes = self.__database.select_many( Note, Notebook.sql_search_titles( notebook_id, search_text ) )
for note in notes:
note.summary = note.summary.replace( search_text, u"<b>%s</b>" % search_text )
return dict(
notes = notes,
)
@expose( view = Json ) @expose( view = Json )
@strongly_expire @strongly_expire
@end_transaction @end_transaction

View File

@ -1,7 +1,7 @@
import re import re
from copy import copy from copy import copy
from Note import Note from Note import Note
from Persistent import Persistent, quote from Persistent import Persistent, quote, quote_fuzzy
class Notebook( Persistent ): class Notebook( Persistent ):
@ -200,6 +200,33 @@ class Notebook( Persistent ):
""" % ( quote( search_text ), quote( user_id ), """ % ( quote( search_text ), quote( user_id ),
quote( first_notebook_id ) ) quote( first_notebook_id ) )
@staticmethod
def sql_search_titles( notebook_id, search_text ):
"""
Return a SQL string to perform a search for notes within the given notebook whose titles contain
the given search_text. This is a case-insensitive search.
@type search_text: unicode
@param search_text: text to search for within the notes
"""
# strip out all search operators
search_text = Notebook.SEARCH_OPERATORS.sub( u"", search_text ).strip()
return \
"""
select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id, null,
title as summary
from
note_current
where
notebook_id = %s and
deleted_from_id is null and
title ilike %s
order by
revision desc limit 20;
""" % ( quote( notebook_id ),
quote_fuzzy( search_text ) )
def sql_highest_note_rank( self ): def sql_highest_note_rank( self ):
""" """
Return a SQL string to determine the highest numbered rank of all notes in this notebook." Return a SQL string to determine the highest numbered rank of all notes in this notebook."

View File

@ -85,3 +85,13 @@ def quote( value ):
value = unicode( value ) value = unicode( value )
return "'%s'" % value.replace( "'", "''" ).replace( "\\", "\\\\" ) return "'%s'" % value.replace( "'", "''" ).replace( "\\", "\\\\" )
def quote_fuzzy( value ):
if value is None:
return "null"
value = unicode( value )
value = value.replace( "'", "''" ).replace( "\\", "\\\\" )
return "'%" + value + "%'"

View File

@ -514,6 +514,7 @@ h1 {
.pulldown { .pulldown {
position: absolute; position: absolute;
font-size: 72%;
text-align: left; text-align: left;
max-height: 20em; max-height: 20em;
overflow: auto; overflow: auto;
@ -532,6 +533,14 @@ h1 {
text-decoration: none; text-decoration: none;
} }
.suggestion {
padding: 0.25em 0.5em 0.25em 0.5em;
}
.selected_suggestion {
background-color: #d0e0f0;
}
.pulldown_label { .pulldown_label {
color: #000000; color: #000000;
} }

View File

@ -445,7 +445,7 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_
if ( link ) { if ( link ) {
var pulldown = link.pulldown; var pulldown = link.pulldown;
var pulldown_title = undefined; var pulldown_title = undefined;
if ( pulldown ) { if ( pulldown && pulldown.title_field ) {
pulldown_title = strip( pulldown.title_field.value ); pulldown_title = strip( pulldown.title_field.value );
if ( pulldown_title ) if ( pulldown_title )
note_title = pulldown_title; note_title = pulldown_title;
@ -823,16 +823,44 @@ Wiki.prototype.display_link_pulldown = function ( editor, link, ephemeral ) {
if ( !link ) if ( !link )
link = editor.find_link_at_cursor(); link = editor.find_link_at_cursor();
// if there's no link at the current cursor location, or there is a link but it was just started, // if there's no link at the current cursor location, bail
// bail if ( !link ) {
if ( !link || link == editor.link_started ) {
this.clear_pulldowns(); this.clear_pulldowns();
return; return;
} }
var pulldown = link.pulldown; var pulldown = link.pulldown;
if ( pulldown ) var query = parse_query( link );
pulldown.update_position(); var title = link_title( link, query );
// display a Suggest_pulldown once something is typed for the link title, as long as the link
// doesn't yet have a note_id
if ( !pulldown && title.length > 0 && query.note_id == "new" ) {
this.clear_pulldowns();
var self = this;
var suggest_pulldown = new Suggest_pulldown( this, this.notebook_id, this.invoker, editor, link, title, editor.document );
connect( suggest_pulldown, "suggestion_selected", function ( note ) {
self.update_link_with_suggestion( editor, link, note )
} );
return;
}
// if there is a link but it was just started, bail
if ( link == editor.link_started && !pulldown ) {
this.clear_pulldowns();
return;
}
if ( pulldown ) {
// if a Suggest_pulldown is open for the link, update the pulldown and bail
if ( pulldown.update_suggestions ) {
pulldown.update_suggestions( title );
return;
// otherwise, just update the pulldown's position
} else {
pulldown.update_position();
}
}
var link_contains_image = getElementsByTagAndClassName( "img", null, link ); var link_contains_image = getElementsByTagAndClassName( "img", null, link );
@ -853,6 +881,26 @@ Wiki.prototype.display_link_pulldown = function ( editor, link, ephemeral ) {
} }
} }
Wiki.prototype.update_link_with_suggestion = function ( editor, link, note ) {
link.innerHTML = note.title;
// manually position the text cursor at the end of the link title
if ( editor.iframe.contentWindow && editor.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = editor.iframe.contentWindow.getSelection();
selection.selectAllChildren( link );
selection.collapseToEnd();
}
link.href = "/notebooks/" + this.notebook_id + "?note_id=" + note.object_id;
link.pulldown.shutdown();
link.pulldown = null;
editor.focus();
editor.end_link();
this.display_link_pulldown( editor, link );
}
Wiki.prototype.editor_focused = function ( editor, synchronous ) { Wiki.prototype.editor_focused = function ( editor, synchronous ) {
if ( editor ) if ( editor )
addElementClass( editor.iframe, "focused_note_frame" ); addElementClass( editor.iframe, "focused_note_frame" );
@ -1181,6 +1229,7 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
if ( existing_div ) { if ( existing_div ) {
existing_div.pulldown.shutdown(); existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return; return;
} }
@ -1477,7 +1526,6 @@ Wiki.prototype.submit_form = function ( form ) {
} else if ( url == "/users/signup_group_member" ) { } else if ( url == "/users/signup_group_member" ) {
callback = function ( result ) { callback = function ( result ) {
var group_id = getFirstElementByTagAndClassName( "input", "group_id", form ).value; var group_id = getFirstElementByTagAndClassName( "input", "group_id", form ).value;
console.log( form, group_id );
self.invoker.invoke( "/groups/load_users", "GET", { self.invoker.invoke( "/groups/load_users", "GET", {
"group_id": group_id "group_id": group_id
}, function ( result ) { }, function ( result ) {
@ -2189,6 +2237,7 @@ Wiki.prototype.clear_pulldowns = function ( ephemeral_only ) {
continue; continue;
result.pulldown.shutdown(); result.pulldown.shutdown();
result.pulldown = null;
} }
} }
} }
@ -2443,6 +2492,7 @@ Wiki.prototype.toggle_editor_changes = function ( event, editor ) {
var existing_div = getElement( pulldown_id ); var existing_div = getElement( pulldown_id );
if ( existing_div ) { if ( existing_div ) {
existing_div.pulldown.shutdown(); existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return; return;
} }
@ -2475,6 +2525,7 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) {
var existing_div = getElement( pulldown_id ); var existing_div = getElement( pulldown_id );
if ( existing_div ) { if ( existing_div ) {
existing_div.pulldown.shutdown(); existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return; return;
} }
@ -3306,6 +3357,149 @@ File_link_pulldown.prototype.shutdown = function () {
disconnectAll( this.right_justify_radio ); disconnectAll( this.right_justify_radio );
} }
function Suggest_pulldown( wiki, notebook_id, invoker, editor, anchor, search_text, key_press_node ) {
anchor.pulldown = this;
this.anchor = anchor;
this.previous_search_text = "";
Pulldown.call( this, wiki, notebook_id, "suggest_" + editor.id, anchor, editor.iframe );
this.invoker = invoker;
this.editor = editor;
this.update_suggestions( search_text );
var self = this;
this.key_handler = connect( key_press_node, "onkeydown", function ( event ) { self.key_pressed( event ); } );
Pulldown.prototype.update_position.call( this );
}
Suggest_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
Suggest_pulldown.prototype.constructor = Suggest_pulldown;
Suggest_pulldown.prototype.update_suggestions = function ( search_text ) {
// if the search text hasn't changed since last time, bail
if ( this.previous_search_text == search_text )
return;
// if there is no search text, hide the pulldown and bail
if ( !search_text ) {
addElementClass( this.div, "invisible" );
return;
}
var self = this;
this.previous_search_text = search_text;
this.invoker.invoke( "/notebooks/search_titles", "GET", {
"notebook_id": this.notebook_id,
"search_text": search_text
},
function( result ) { self.display_suggestions( result, search_text ); }
);
}
Suggest_pulldown.prototype.display_suggestions = function ( result, search_text ) {
if ( result.notes.length == 0 ) {
addElementClass( this.div, "invisible" );
return;
}
removeElementClass( this.div, "invisible" );
var results_list = createDOM( "div" );
var self = this;
function connect_link( suggest_link, note ) {
connect( suggest_link, "onclick", function ( event ) { self.suggestion_selected( event, note ); } );
}
for ( var i in result.notes ) {
var note = result.notes[ i ];
if ( !note.title ) continue;
var suggest_link = createDOM( "a", { "href": "#", "class": "pulldown_link" } );
suggest_link.innerHTML = note.summary;
suggest_link.note = note;
appendChildNodes( results_list, createDOM( "div", { "class": "suggestion" }, suggest_link ) );
connect_link( suggest_link, note );
}
replaceChildNodes( this.div, results_list );
}
Suggest_pulldown.prototype.suggestion_selected = function ( event, note ) {
event.stop();
signal( this, "suggestion_selected", note );
}
Suggest_pulldown.prototype.key_pressed = function ( event ) {
// an invisible Suggest_pulldown shouldn't grab keypresses
if ( hasElementClass( this.div, "invisible" ) )
return;
var code = event.key().code;
// up arrow: move up to the previous suggestion
if ( code == 38 ) {
var selected = getFirstElementByTagAndClassName( "div", "selected_suggestion", this.div );
// if something is selected and there's a previous suggestion in the list, move the selection up
if ( selected && selected.previousSibling ) {
removeElementClass( selected, "selected_suggestion" );
addElementClass( selected.previousSibling, "selected_suggestion" );
// otherwise, hide the Suggest_pulldown
} else {
addElementClass( this.div, "invisible" );
}
// down arrow: move down to the previous suggestion
} else if ( code == 40 ) {
var selected = getFirstElementByTagAndClassName( "div", "selected_suggestion", this.div );
// if something is selected and there's a next suggestion in the list, move the selection down
if ( selected ) {
if ( selected.nextSibling ) {
removeElementClass( selected, "selected_suggestion" );
addElementClass( selected.nextSibling, "selected_suggestion" );
}
// if nothing is selected yet, then just select the first link
} else {
var suggest_link = getFirstElementByTagAndClassName( "a", "pulldown_link", this.div );
addElementClass( suggest_link.parentNode, "selected_suggestion" );
}
// enter: select current suggestion
} else if ( code == 13 ) {
var selected = getFirstElementByTagAndClassName( "div", "selected_suggestion", this.div );
var suggest_link = getFirstElementByTagAndClassName( "a", "pulldown_link", selected );
if ( selected )
this.suggestion_selected( event, suggest_link.note );
// escape: hide the suggestions
} else if ( code == 27 ) {
addElementClass( this.div, "invisible" );
// otherwise, not a key this method handles
} else {
return;
}
event.stop();
}
Suggest_pulldown.prototype.update_position = function ( anchor, relative_to ) {
Pulldown.prototype.update_position.call( this, anchor, relative_to );
}
Suggest_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this );
this.anchor.pulldown = null;
disconnectAll( this );
disconnect( this.key_handler );
}
function Note_tree( wiki, notebook_id, invoker ) { function Note_tree( wiki, notebook_id, invoker ) {
this.wiki = wiki; this.wiki = wiki;
this.notebook_id = notebook_id; this.notebook_id = notebook_id;