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,
)
@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 )
@strongly_expire
@end_transaction

View File

@ -1,7 +1,7 @@
import re
from copy import copy
from Note import Note
from Persistent import Persistent, quote
from Persistent import Persistent, quote, quote_fuzzy
class Notebook( Persistent ):
@ -200,6 +200,33 @@ class Notebook( Persistent ):
""" % ( quote( search_text ), quote( user_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 ):
"""
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 )
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 {
position: absolute;
font-size: 72%;
text-align: left;
max-height: 20em;
overflow: auto;
@ -532,6 +533,14 @@ h1 {
text-decoration: none;
}
.suggestion {
padding: 0.25em 0.5em 0.25em 0.5em;
}
.selected_suggestion {
background-color: #d0e0f0;
}
.pulldown_label {
color: #000000;
}

View File

@ -445,7 +445,7 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_
if ( link ) {
var pulldown = link.pulldown;
var pulldown_title = undefined;
if ( pulldown ) {
if ( pulldown && pulldown.title_field ) {
pulldown_title = strip( pulldown.title_field.value );
if ( pulldown_title )
note_title = pulldown_title;
@ -823,16 +823,44 @@ Wiki.prototype.display_link_pulldown = function ( editor, link, ephemeral ) {
if ( !link )
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,
// bail
if ( !link || link == editor.link_started ) {
// if there's no link at the current cursor location, bail
if ( !link ) {
this.clear_pulldowns();
return;
}
var pulldown = link.pulldown;
if ( pulldown )
pulldown.update_position();
var query = parse_query( link );
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 );
@ -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 ) {
if ( editor )
addElementClass( editor.iframe, "focused_note_frame" );
@ -1181,6 +1229,7 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
if ( existing_div ) {
existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return;
}
@ -1477,7 +1526,6 @@ Wiki.prototype.submit_form = function ( form ) {
} else if ( url == "/users/signup_group_member" ) {
callback = function ( result ) {
var group_id = getFirstElementByTagAndClassName( "input", "group_id", form ).value;
console.log( form, group_id );
self.invoker.invoke( "/groups/load_users", "GET", {
"group_id": group_id
}, function ( result ) {
@ -2189,6 +2237,7 @@ Wiki.prototype.clear_pulldowns = function ( ephemeral_only ) {
continue;
result.pulldown.shutdown();
result.pulldown = null;
}
}
}
@ -2443,6 +2492,7 @@ Wiki.prototype.toggle_editor_changes = function ( event, editor ) {
var existing_div = getElement( pulldown_id );
if ( existing_div ) {
existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return;
}
@ -2475,6 +2525,7 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) {
var existing_div = getElement( pulldown_id );
if ( existing_div ) {
existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return;
}
@ -3306,6 +3357,149 @@ File_link_pulldown.prototype.shutdown = function () {
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 ) {
this.wiki = wiki;
this.notebook_id = notebook_id;