witten
/
luminotes
Archived
1
0
Fork 0

Implemented support for embedded images within wiki notes. Also added hover support to open link pulldowns.

This commit is contained in:
Dan Helfman 2008-06-13 15:55:19 -07:00
parent 7a5732c776
commit 522f8b9330
7 changed files with 184 additions and 18 deletions

5
NEWS
View File

@ -1,6 +1,11 @@
1.4.1: June ?, 2008:
* Implemented support for embedded images within wiki notes.
* You can now open a link pulldown by simply hovering the mouse over a link
for a few seconds.
* Updated tools/set_plan.py to automatically update a user's group
membership.
* Removed Google AdWords <script> tag from distributed Luminotes tarball,
as not every installation of Luminotes uses AdWords.
1.4.0: June 9, 2008
* Implemented some basic user administration features, allowing you to

View File

@ -206,7 +206,7 @@ cherrypy._cpcgifs.FieldStorage = FieldStorage
class Files( object ):
FILE_LINK_PATTERN = re.compile( u'<a\s+href="[^"]*/files/download\?file_id=([^"&]+)(&[^"]*)?">[^<]+</a>', re.IGNORECASE )
FILE_LINK_PATTERN = re.compile( u'<a\s+href="[^"]*/files/download\?file_id=([^"&]+)(&[^"]*)?">.+</a>', re.IGNORECASE )
"""
Controller for dealing with uploaded files, corresponding to the "/files" URL.

View File

@ -143,7 +143,7 @@ class Html_cleaner(HTMLParser):
self.allowed_attributes = {
'a': [ 'href', 'target' ],
'p': [ 'align' ],
'img': [ 'alt', 'border', 'title' ],
'img': [ 'src', 'alt', 'border', 'title', "class" ],
'table': [ 'cellpadding', 'cellspacing', 'border', 'width', 'height' ],
'font': [ 'color', 'size', 'face' ],
'td': [ 'rowspan', 'colspan', 'width', 'height' ],

View File

@ -1577,6 +1577,33 @@ class Test_files( Test_controller ):
assert db_file.filename == self.filename
assert Upload_file.exists( self.file_id )
def test_purge_unused_keep_image_file( self ):
self.login()
self.http_upload(
"/files/upload?file_id=%s" % self.file_id,
dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
),
filename = self.filename,
file_data = self.file_data,
content_type = self.content_type,
session_id = self.session_id,
)
self.note.contents = '<a href="/files/download?file_id=%s"><img src="/blah"></a>' % self.file_id
self.database.save( self.note )
# the image file is linked to from the note's contents, so this should not delete it
cherrypy.root.files.purge_unused( self.note )
db_file = self.database.load( File, self.file_id )
assert db_file
assert db_file.object_id == self.file_id
assert db_file.filename == self.filename
assert Upload_file.exists( self.file_id )
def test_purge_unused_keep_file_with_quote_filename( self ):
self.login()

View File

@ -40,6 +40,31 @@ del a {
color: red;
}
img {
border-width: 0;
}
img .thumbnail_left {
float: left;
padding-right: 1.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
img .thumbnail_center {
display: block;
margin-left: auto;
margin-right: auto;
padding: 0.5em;
}
img .thumbnail_right {
float: right;
padding-left: 1.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
hr {
border: 0;
color: #000000;

View File

@ -14,6 +14,8 @@ function Editor( id, notebook_id, note_text, deleted_from_id, revision, read_wri
this.closed = false;
this.link_started = null;
this.gecko = /Gecko/.test( navigator.userAgent ) && !/like Gecko/.test( navigator.userAgent );
this.hover_target = null;
this.hover_timer = null;
var iframe_id = "note_" + id;
var self = this;
@ -154,6 +156,8 @@ Editor.prototype.finish_init = function () {
connect( this.iframe.contentWindow, "onblur", function ( event ) { self.blurred( event ); } );
connect( this.iframe.contentWindow, "onfocus", function ( event ) { self.focused( event ); } );
connect( this.document, "onclick", function ( event ) { self.mouse_clicked( event ); } );
connect( this.document, "onmouseover", function ( event ) { self.mouse_hovered( event ); } );
connect( this.document, "ondragover", function ( event ) { self.mouse_dragged( event ); } );
// handle each form submit event by forwarding it on as a custom event
function connect_form( form ) {
@ -376,6 +380,46 @@ Editor.prototype.mouse_clicked = function ( event ) {
signal( this, "state_changed", this, link_clicked );
}
HOVER_DURATION_MILLISECONDS = 1000;
Editor.prototype.mouse_hovered = function ( event ) {
// search through the tree of elements containing the hover target for a link
var link = event.target()
if ( !link ) false;
while ( link.nodeName != "A" ) {
link = link.parentNode;
if ( !link )
break;
}
if ( !link || !link.href )
link = null;
var self = this;
var hover_target = link || event.target();
this.hover_target = hover_target;
if ( this.hover_timer )
clearTimeout( this.hover_timer );
this.hover_timer = setTimeout( function () { self.mouse_hover_timeout( hover_target ) }, HOVER_DURATION_MILLISECONDS );
}
Editor.prototype.mouse_hover_timeout = function ( hover_target ) {
// if the mouse is hovering over the same target that it was when the timer started
if ( hover_target == this.hover_target )
signal( this, "mouse_hovered", hover_target );
}
Editor.prototype.mouse_dragged = function ( event ) {
// reset the hover timer to prevent a mouse hover from being registered while the mouse is being dragged
if ( this.hover_timer ) {
var self = this;
clearTimeout( this.hover_timer );
this.hover_timer = setTimeout( function () { self.mouse_hover_timeout( self.hover_target ) }, HOVER_DURATION_MILLISECONDS );
}
}
Editor.prototype.scrape_title = function () {
// scrape the note title out of the editor
var heading = getFirstElementByTagAndClassName( "h3", null, this.document );

View File

@ -728,6 +728,7 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
connect( editor, "changes_clicked", function ( event ) { self.toggle_editor_changes( event, editor ) } );
connect( editor, "options_clicked", function ( event ) { self.toggle_editor_options( event, editor ) } );
connect( editor, "focused", this, "editor_focused" );
connect( editor, "mouse_hovered", function ( target ) { self.editor_mouse_hovered( editor, target ) } );
}
connect( editor, "load_editor", this, "load_editor" );
@ -800,7 +801,7 @@ Wiki.prototype.editor_title_changed = function ( editor, old_title, new_title )
}
}
Wiki.prototype.display_link_pulldown = function ( editor, link ) {
Wiki.prototype.display_link_pulldown = function ( editor, link, ephemeral ) {
this.clear_messages();
if ( !editor.read_write ) {
@ -822,18 +823,20 @@ Wiki.prototype.display_link_pulldown = function ( editor, link ) {
if ( pulldown )
pulldown.update_position();
var link_contains_image = getElementsByTagAndClassName( "img", null, link );
// if the cursor is now on a link, display a link pulldown if there isn't already one open
if ( link_title( link ).length > 0 ) {
if ( link_title( link ).length > 0 || link_contains_image ) {
if ( !pulldown ) {
this.clear_pulldowns();
// display a different pulldown depending on whether the link is a note link or a file link
if ( link.target || !/\/files\//.test( link.href ) )
new Link_pulldown( this, this.notebook_id, this.invoker, editor, link );
new Link_pulldown( this, this.notebook_id, this.invoker, editor, link, ephemeral );
else {
if ( /\/files\/new$/.test( link.href ) )
new Upload_pulldown( this, this.notebook_id, this.invoker, editor, link );
new Upload_pulldown( this, this.notebook_id, this.invoker, editor, link, ephemeral );
else
new File_link_pulldown( this, this.notebook_id, this.invoker, editor, link );
new File_link_pulldown( this, this.notebook_id, this.invoker, editor, link, ephemeral );
}
}
}
@ -867,6 +870,15 @@ Wiki.prototype.editor_focused = function ( editor, synchronous ) {
}
}
Wiki.prototype.editor_mouse_hovered = function ( editor, target ) {
// if the mouse is hovering over a link, open a link pulldown
if ( target.nodeName == "A" )
this.display_link_pulldown( editor, target, true );
// the mouse is hovering over something else, so clear all ephemeral pulldowns
else
this.clear_pulldowns( true );
}
Wiki.prototype.key_pressed = function ( event ) {
if ( !this.notebook.read_write )
return;
@ -2151,15 +2163,19 @@ Wiki.prototype.clear_messages = function () {
}
}
Wiki.prototype.clear_pulldowns = function () {
Wiki.prototype.clear_pulldowns = function ( ephemeral_only ) {
var results = getElementsByTagAndClassName( "div", "pulldown" );
for ( var i in results ) {
var result = results[ i ];
// close the pulldown if it's been open at least a quarter second
if ( new Date() - result.pulldown.init_time >= 250 )
if ( new Date() - result.pulldown.init_time >= 250 ) {
if ( ephemeral_only && !result.pulldown.ephemeral )
continue;
result.pulldown.shutdown();
}
}
}
@ -2455,7 +2471,7 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) {
connect( window, "onload", function ( event ) { new Wiki( new Invoker() ); } );
function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to ) {
function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to, ephemeral ) {
this.wiki = wiki;
this.notebook_id = notebook_id;
this.div = createDOM( "div", { "id": pulldown_id, "class": "pulldown" } );
@ -2463,6 +2479,7 @@ function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to ) {
this.init_time = new Date();
this.anchor = anchor;
this.relative_to = relative_to;
this.ephemeral = ephemeral;
addElementClass( this.div, "invisible" );
@ -2471,6 +2488,15 @@ function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to ) {
setElementPosition( this.div, position );
removeElementClass( this.div, "invisible" );
if ( this.ephemeral ) {
// when the mouse cursor is moved into the pulldown, it becomes non-ephemeral (in other words,
// it will no longer disappear in a few seconds)
var self = this;
connect( this.div, "onmouseover", function ( event ) {
self.ephemeral = false;
} );
}
}
function calculate_position( anchor, relative_to ) {
@ -2608,11 +2634,11 @@ Changes_pulldown.prototype.shutdown = function () {
}
function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
function Link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ) {
link.pulldown = this;
this.link = link;
Pulldown.call( this, wiki, notebook_id, "link_" + editor.id, link, editor.iframe );
Pulldown.call( this, wiki, notebook_id, "link_" + editor.id, link, editor.iframe, ephemeral );
this.invoker = invoker;
this.editor = editor;
@ -2772,11 +2798,11 @@ Link_pulldown.prototype.shutdown = function () {
this.link.pulldown = null;
}
function Upload_pulldown( wiki, notebook_id, invoker, editor, link ) {
function Upload_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ) {
this.link = link || editor.find_link_at_cursor();
this.link.pulldown = this;
Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, this.link, editor.iframe );
Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, this.link, editor.iframe, ephemeral );
wiki.down_image_button( "attachFile" );
this.invoker = invoker;
@ -2823,6 +2849,10 @@ Upload_pulldown.prototype.init_frame = function () {
self.upload_started( getElement( "file_id" ).value );
} );
} );
connect( doc.body, "onmouseover", function ( event ) {
self.ephemeral = false;
} );
} );
}
@ -2914,17 +2944,18 @@ Upload_pulldown.prototype.shutdown = function () {
this.link.pulldown = null;
}
function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) {
function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ) {
link.pulldown = this;
this.link = link;
Pulldown.call( this, wiki, notebook_id, "file_link_" + editor.id, link, editor.iframe );
Pulldown.call( this, wiki, notebook_id, "file_link_" + editor.id, link, editor.iframe, ephemeral );
this.invoker = invoker;
this.editor = editor;
this.filename_field = createDOM( "input", { "class": "text_field", "size": "30", "maxlength": "256" } );
this.file_size = createDOM( "span", {} );
this.previous_filename = "";
this.link_title = null;
var self = this;
connect( this.filename_field, "onclick", function ( event ) { self.filename_field_clicked( event ); } );
@ -2948,17 +2979,31 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) {
else
var quote_filename = false;
appendChildNodes( this.div, createDOM( "span", {},
this.thumbnail_span = createDOM( "span", {},
createDOM( "a", { href: "/files/download?file_id=" + this.file_id + "&quote_filename=" + quote_filename, target: "_new" },
createDOM( "img", { "src": "/files/thumbnail?file_id=" + this.file_id, "class": "file_thumbnail" } )
)
) );
);
appendChildNodes( this.div, this.thumbnail_span );
// if the link is an image thumbnail link, update the contents of the file link pulldown accordingly
if ( getElementsByTagAndClassName( "img", null, this.link ).length > 0 ) {
this.embed_checkbox = createDOM( "input", { "type": "checkbox", "class": "pulldown_checkbox", "id": "embed_checkbox", "checked": "true" } );
addElementClass( this.thumbnail_span, "undisplayed" );
} else {
this.embed_checkbox = createDOM( "input", { "type": "checkbox", "class": "pulldown_checkbox", "id": "embed_checkbox" } );
}
var embed_label = createDOM( "label", { "for": "embed_checkbox", "class": "pulldown_label", "title": "Embed this image within the note itself." },
"show image within note"
);
appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "filename: " ) );
appendChildNodes( this.div, this.filename_field );
appendChildNodes( this.div, this.file_size );
appendChildNodes( this.div, " " );
appendChildNodes( this.div, delete_button );
appendChildNodes( this.div, createDOM( "div", {}, this.embed_checkbox, embed_label ) );
// get the file's name and size from the server
this.invoker.invoke(
@ -2977,6 +3022,7 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) {
);
connect( delete_button, "onclick", function ( event ) { self.delete_button_clicked( event ); } );
connect( this.embed_checkbox, "onclick", function ( event ) { self.embed_clicked( event ); } );
// FIXME: when this is called, the text cursor moves to an unexpected location
editor.focus();
@ -3025,6 +3071,10 @@ File_link_pulldown.prototype.filename_field_key_pressed = function ( event ) {
File_link_pulldown.prototype.delete_button_clicked = function ( event ) {
var self = this;
// change the embedded image (if any) back into a plain file link before deletion
if ( getElementsByTagAndClassName( "img", null, this.link ).length > 0 )
this.link.innerHTML = this.link_title || this.filename_field.value || this.previous_filename;
this.invoker.invoke(
"/files/delete", "POST", {
"file_id": this.file_id
@ -3038,6 +3088,21 @@ File_link_pulldown.prototype.delete_button_clicked = function ( event ) {
this.wiki.display_message( 'The file "' + strip( this.filename_field.value ) + '" has been deleted.' );
}
File_link_pulldown.prototype.embed_clicked = function ( event ) {
if ( this.embed_checkbox.checked ) {
var image = createDOM( "img", { "src": "/files/thumbnail?file_id=" + this.file_id, "class": "thumbnail_left" } );
var image_span = createDOM( "span", {}, image );
this.link_title = link_title( this.link );
this.link.innerHTML = image_span.innerHTML;
addElementClass( this.thumbnail_span, "undisplayed" );
} else {
removeElementClass( this.thumbnail_span, "undisplayed" );
this.link.innerHTML = this.link_title || this.filename_field.value || this.previous_filename;
}
this.update_position( this.link, this.editor.iframe );
}
File_link_pulldown.prototype.update_position = function ( anchor, relative_to ) {
Pulldown.prototype.update_position.call( this, anchor, relative_to );
}