function Wiki() { this.next_id = null; this.focused_editor = null; this.blank_editor_id = null; this.notebook = null; this.notebook_id = getElement( "notebook_id" ).value; this.read_write = false; this.startup_entries = new Array(); // map of startup entries: entry id to bool this.invoker = new Invoker(); connect( this.invoker, "error_message", this, "display_message" ); connect( "search_form", "onsubmit", this, "search" ); // get info on the requested notebook (if any) var self = this; if ( this.notebook_id ) { this.invoker.invoke( "/notebooks/contents", "GET", { "notebook_id": this.notebook_id }, function( result ) { self.populate( result ); } ); } // get info on the current user (logged-in or anonymous) this.invoker.invoke( "/users/current", "GET", null, function( result ) { self.display_user( result ); } ); } Wiki.prototype.update_next_id = function ( result ) { this.next_id = result.next_id; } Wiki.prototype.display_user = function ( result ) { // if no notebook id was requested, then just display the user's default notebook if ( !this.notebook_id ) { this.notebook_id = result.notebooks[ 0 ].object_id; this.populate( { "notebook" : result.notebooks[ 0 ] } ); } if ( result.user.username == "anonymous" ) return; // display links for current notebook a list of other notebooks that the user has access to var span = createDOM( "span" ); replaceChildNodes( "notebook_area", span ); appendChildNodes( span, createDOM( "a", { "href": "/notebooks/" + this.notebook_id, "id": "recent_entries_link" }, "recent entries" ) ); appendChildNodes( span, createDOM( "br" ) ); appendChildNodes( span, createDOM( "a", { "href": "/notebooks/download_html/" + this.notebook_id, "id": "download_html_link" }, "download as html" ) ); appendChildNodes( span, createDOM( "h3", "other notebooks" ) ); for ( var i in result.notebooks ) { var notebook = result.notebooks[ i ]; if ( notebook.object_id != this.notebook_id ) { appendChildNodes( span, createDOM( "a", { "href": ( notebook.name == "Luminotes" ) ? "/" : "/notebooks/" + notebook.object_id, "id": "notebook_" + notebook.object_id }, notebook.name ) ); appendChildNodes( span, createDOM( "br" ) ); } } // display the name of the logged in user and a logout link span = createDOM( "span" ); replaceChildNodes( "user_area", span ); appendChildNodes( span, "logged in as " + result.user.username ); appendChildNodes( span, " | " ); appendChildNodes( span, createDOM( "a", { "href": "/", "id": "logout_link" }, "logout" ) ); var self = this; connect( "recent_entries_link", "onclick", function ( event ) { self.invoker.invoke( "/notebooks/recent_entries", "GET", { "notebook_id": self.notebook_id }, function( result ) { self.display_search_results( result ); } ); event.stop(); } ); connect( "download_html_link", "onclick", function ( event ) { self.save_editor( null, true ); } ); connect( "logout_link", "onclick", function ( event ) { self.save_editor( null, true ); self.invoker.invoke( "/users/logout", "POST" ); event.stop(); } ); } Wiki.prototype.populate = function ( result ) { this.notebook = result.notebook; var self = this; if ( this.notebook.name != "Luminotes" ) replaceChildNodes( "notebook_name", createDOM( "h3", this.notebook.name ) ); if ( this.notebook.read_write ) { this.read_write = true; removeElementClass( "toolbar", "undisplayed" ); connect( window, "onunload", function ( event ) { self.editor_focused( null, true ); } ); connect( "bold", "onclick", function ( event ) { self.toggle_button( event, "bold" ); } ); connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } ); connect( "title", "onclick", function ( event ) { self.toggle_button( event, "title", "h3" ); } ); connect( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } ); connect( "insertOrderedList", "onclick", function ( event ) { self.toggle_button( event, "insertOrderedList" ); } ); connect( "createLink", "onclick", this, "toggle_link_button" ); connect( "newEntry", "onclick", this, "create_blank_editor" ); connect( "html", "onclick", this, "background_clicked" ); // grab the next available object id this.invoker.invoke( "/next_id", "POST", null, function( result ) { self.update_next_id( result ); } ); } // create an editor for each startup entry in the received notebook, focusing the first one for ( var i in this.notebook.startup_entries ) { var entry = this.notebook.startup_entries[ i ]; if ( !entry ) continue; this.startup_entries[ entry.object_id ] = true; var focus = ( i == 0 ); this.create_editor( entry.object_id, entry.contents, undefined, undefined, false, focus ); } } Wiki.prototype.background_clicked = function ( event ) { this.clear_pulldowns(); // unless a background div was clicked, bail var node_name = event.target().nodeName.toLowerCase(); if ( node_name != "div" && node_name != "html" ) return; this.create_blank_editor( event ); } Wiki.prototype.create_blank_editor = function ( event ) { if ( event ) event.stop(); // if there is already a blank editor, then highlight it and bail if ( this.blank_editor_id != null ) { var blank_iframe_id = "entry_" + this.blank_editor_id; var iframe = getElement( blank_iframe_id ); if ( iframe && iframe.editor.empty() ) { iframe.editor.highlight(); return; } } this.blank_editor_id = this.create_editor( undefined, undefined, undefined, undefined, true, true ); } Wiki.prototype.load_editor = function ( entry_title, insert_after_iframe_id, entry_id ) { var self = this; this.invoker.invoke( "/notebooks/load_entry", "GET", { "notebook_id": this.notebook_id, "entry_id": entry_id }, function ( result ) { self.parse_loaded_editor( result, insert_after_iframe_id, entry_title ); } ); } Wiki.prototype.load_editor_by_title = function ( entry_title, insert_after_iframe_id ) { var self = this; this.invoker.invoke( "/notebooks/load_entry_by_title", "GET", { "notebook_id": this.notebook_id, "entry_title": entry_title }, function ( result ) { self.parse_loaded_editor( result, insert_after_iframe_id, entry_title ); } ); } Wiki.prototype.parse_loaded_editor = function ( result, insert_after_iframe_id, entry_title ) { if ( result.entry ) { var id = result.entry.object_id var entry_text = result.entry.contents; } else { var id = null; var entry_text = "

" + entry_title; } this.create_editor( id, entry_text, insert_after_iframe_id, entry_title, true, false ); } Wiki.prototype.create_editor = function ( id, entry_text, insert_after_iframe_id, entry_title, highlight, focus ) { this.clear_messages(); this.clear_pulldowns(); var self = this; if ( isUndefinedOrNull( id ) ) { if ( this.read_write ) { id = this.next_id; this.invoker.invoke( "/next_id", "POST", null, function( result ) { self.update_next_id( result ); } ); } else { id = 0; } } // update any matching links in insert_after_iframe_id with the id of this new editor if ( insert_after_iframe_id ) { var links = getElementsByTagAndClassName( "a", null, getElement( insert_after_iframe_id ).editor.document ); for ( var i in links ) { // a link matches if its contained text is the same as this entry's title if ( scrapeText( links[ i ] ) == entry_title ) links[ i ].href = "/entries/" + id; } } // if an iframe has been given to insert this new editor after, then hide all subsequent non-startup editors if ( insert_after_iframe_id ) { var sibling = getElement( insert_after_iframe_id ).nextSibling; while ( sibling ) { var nextSibling = sibling.nextSibling; if ( sibling.editor && ( this.read_write || !sibling.editor.startup ) ) sibling.editor.shutdown(); sibling = nextSibling; } } var startup = this.startup_entries[ id ]; var editor = new Editor( id, entry_text, undefined, this.read_write, startup, highlight, focus ); if ( this.read_write ) { connect( editor, "state_changed", this, "editor_state_changed" ); connect( editor, "key_pressed", this, "editor_key_pressed" ); connect( editor, "delete_clicked", function ( event ) { self.delete_editor( event, editor ) } ); connect( editor, "options_clicked", function ( event ) { self.toggle_editor_options( event, editor ) } ); connect( editor, "focused", this, "editor_focused" ); } connect( editor, "load_editor", this, "load_editor" ); connect( editor, "load_editor_by_title", this, "load_editor_by_title" ); 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 ); } ); return id; } Wiki.prototype.editor_state_changed = function ( editor ) { this.update_toolbar(); } Wiki.prototype.editor_focused = function ( editor, fire_and_forget ) { this.clear_messages(); this.clear_pulldowns(); if ( editor ) addElementClass( editor.iframe, "focused_entry_frame" ); if ( this.focused_editor && this.focused_editor != editor ) { removeElementClass( this.focused_editor.iframe, "focused_entry_frame" ); // if the formerly focused editor is completely empty, then remove it as the user leaves it and switches to this editor if ( this.focused_editor.empty() ) { this.focused_editor.shutdown(); } else { // when switching editors, save the one being left this.save_editor( null, fire_and_forget ); } } this.focused_editor = editor; } Wiki.prototype.editor_key_pressed = function ( editor, event ) { var code = event.key().code; if ( event.modifier().ctrl ) { // ctrl-backtick: alert with frame HTML contents (temporary for debugging) if ( code == 192 || code == 96 ) { alert( editor.document.body.innerHTML ); event.stop(); // ctrl-b: bold } else if ( code == 66 ) { this.toggle_button( event, "bold" ); // ctrl-i: italic } else if ( code == 73 ) { this.toggle_button( event, "italic" ); // ctrl-t: title } else if ( code == 84 ) { this.toggle_button( event, "title", "h3" ); // ctrl-l: unordered list } else if ( code == 76 ) { this.toggle_button( event, "insertUnorderedList" ); // ctrl-n: ordered list } else if ( code == 49 ) { this.toggle_button( event, "insertOrderedList" ); // ctrl-e: make an entry link } else if ( code == 69 ) { this.toggle_link_button( event ); // ctrl-n: new entry } else if ( code == 78 ) { this.create_blank_editor( event ); // ctrl-h: hide entry } else if ( code == 72 ) { this.hide_editor( event ); // ctrl-d: delete entry } else if ( code == 68 ) { this.delete_editor( event ); } // IE: hitting space or tab while making a link shouldn't end the link } else if ( ( code == 32 || code == 9 ) && editor.document.selection && editor.state_enabled( "createLink" ) ) { var range = editor.document.selection.createRange(); var text = range.parentElement().firstChild; text.nodeValue += " "; event.stop(); } } Wiki.prototype.toggle_button = function ( event, button_id, state_name ) { this.clear_messages(); this.clear_pulldowns(); if ( this.focused_editor ) { this.focused_editor.focus(); this.focused_editor.exec_command( state_name || button_id ); this.focused_editor.resize(); this.update_button( button_id, state_name ); } event.stop(); } Wiki.prototype.update_button = function ( button_id, state_name ) { if ( this.focused_editor.state_enabled( state_name || button_id ) ) addElementClass( button_id, "button_down" ); else removeElementClass( button_id, "button_down" ); } Wiki.prototype.update_toolbar = function() { if ( this.focused_editor ) { this.update_button( "bold" ); this.update_button( "italic" ); this.update_button( "title", "h3" ); this.update_button( "insertUnorderedList" ); this.update_button( "insertOrderedList" ); this.update_button( "createLink" ); } } Wiki.prototype.toggle_link_button = function ( event ) { this.clear_messages(); this.clear_pulldowns(); if ( this.focused_editor ) { this.focused_editor.focus(); toggleElementClass( "button_down", "createLink" ); if ( hasElementClass( "createLink", "button_down" ) ) this.focused_editor.start_link(); else this.focused_editor.end_link(); } event.stop(); } Wiki.prototype.hide_editor = function ( event, editor ) { this.clear_messages(); this.clear_pulldowns(); if ( !editor ) { editor = this.focused_editor; this.focused_editor = null; } if ( editor ) { // before hiding an editor, save it if ( this.read_write ) this.save_editor( editor ); editor.shutdown(); } event.stop(); } Wiki.prototype.delete_editor = function ( event, editor ) { this.clear_messages(); this.clear_pulldowns(); if ( !editor ) { editor = this.focused_editor; this.focused_editor = null; } if ( editor ) { if ( this.startup_entries[ editor.id ] ) delete this.startup_entries[ editor.id ]; if ( this.read_write ) { this.invoker.invoke( "/notebooks/delete_entry", "POST", { "notebook_id": this.notebook_id, "entry_id": editor.id } ); } if ( editor == this.focused_editor ) this.focused_editor = null; editor.shutdown(); } event.stop(); } Wiki.prototype.save_editor = function ( editor, fire_and_forget ) { if ( !editor ) editor = this.focused_editor; if ( editor && !editor.empty() ) { // TODO: do something with the result other than just ignoring it this.invoker.invoke( "/notebooks/save_entry", "POST", { "notebook_id": this.notebook_id, "entry_id": editor.id, "contents": editor.contents(), "startup": editor.startup }, null, null, fire_and_forget ); } } Wiki.prototype.search = function ( event ) { this.clear_messages(); this.clear_pulldowns(); var self = this; this.invoker.invoke( "/notebooks/search", "GET", { "notebook_id": this.notebook_id }, function( result ) { self.display_search_results( result ); }, "search_form" ); event.stop(); } Wiki.prototype.display_search_results = function ( result ) { // before displaying the search results, save the current focused editor this.save_editor(); // TODO: somehow highlight the search term within the search results? // make a map of entry object id to entry var entries = {}; for ( var i in result.entries ) { var entry = result.entries[ i ]; entries[ entry.object_id ] = entry; } // hide all existing editors except those for startup entries or search results var iframes = getElementsByTagAndClassName( "iframe", "entry_frame" ); for ( var i in iframes ) { var iframe = iframes[ i ]; // don't hide an existing entry if it's in the search results if ( entries[ iframe.editor.id ] ) { iframe.editor.highlight( false ); delete entries[ iframe.editor.id ]; continue; } // don't hide an existing entry if it's a read-only startup entry if ( iframe.editor.startup && !iframe.editor.read_write ) continue; iframe.editor.shutdown(); } // if there are no search results, indicate that and bail if ( result.entries.length == 0 ) { this.display_message( "No matching entries." ); return; } // create an editor for each entry search result, focusing the first one var i = 0; for ( var id in entries ) { var entry = entries[ id ]; var focus = ( i == 0 ); this.create_editor( id, entry.contents, undefined, undefined, false, focus ); i += 1; } } Wiki.prototype.display_message = function ( text ) { this.clear_messages(); this.clear_pulldowns(); var inner_div = DIV( { "class": "message_inner" }, text ); var div = DIV( { "class": "message" }, inner_div ); appendChildNodes( "entries", div ); ScrollTo( div ); } Wiki.prototype.clear_messages = function () { var results = getElementsByTagAndClassName( "div", "message" ); for ( var i in results ) { var result = results[ i ]; blindUp( result, options = { "duration": 0.5, afterFinish: function () { try { removeElement( result ); } catch ( e ) { } } } ); } } Wiki.prototype.clear_pulldowns = function () { var results = getElementsByTagAndClassName( "div", "pulldown" ); for ( var i in results ) { var result = results[ i ]; result.pulldown.shutdown(); } } Wiki.prototype.toggle_editor_options = function ( event, editor ) { new Pulldown( this.notebook_id, this.invoker, editor ); event.stop(); } connect( window, "onload", function ( event ) { new Wiki(); } ); function Pulldown( notebook_id, invoker, editor ) { // if the pulldown is already open, then just close it var existing_div = getElement( "options_" + editor.id ); if ( existing_div ) { existing_div.pulldown.shutdown(); return; } this.notebook_id = notebook_id; this.invoker = invoker; this.editor = editor; this.div = createDOM( "div", { "id": "options_" + editor.id, "class": "pulldown" } ); this.div.pulldown = this; addElementClass( this.div, "invisible" ); this.close_button = createDOM( "input", { "type": "button", "value": " x ", "class": "pulldown_button" } ); this.startup_checkbox = createDOM( "input", { "type": "checkbox" } ); this.startup_toggle = createDOM( "span", { "class": "pulldown_toggle" }, this.startup_checkbox, "show on startup" ); appendChildNodes( this.div, this.close_button ); appendChildNodes( this.div, this.startup_toggle ); appendChildNodes( document.body, this.div ); this.startup_checkbox.checked = editor.startup; var self = this; connect( this.startup_toggle, "onclick", function ( event ) { self.startup_clicked( event ); event.stop(); } ); connect( this.close_button, "onclick", function ( event ) { self.shutdown(); event.stop(); } ); // position the options pulldown under the options button var position = getElementPosition( editor.options_button ); var options_dimensions = getElementDimensions( editor.options_button ); var div_dimensions = getElementDimensions( this.div ); position.x -= div_dimensions.w - options_dimensions.w; position.y += options_dimensions.h; setElementPosition( this.div, position ); removeElementClass( this.div, "invisible" ); } Pulldown.prototype.startup_clicked = function ( event ) { if ( event.target() != this.startup_checkbox ) this.startup_checkbox.checked = this.startup_checkbox.checked ? false : true; this.editor.startup = this.startup_checkbox.checked; // if this entry isn't empty, save it along with its startup status if ( !this.editor.empty() ) { this.invoker.invoke( "/notebooks/save_entry", "POST", { "notebook_id": this.notebook_id, "entry_id": this.editor.id, "contents": this.editor.contents(), "startup": this.editor.startup } ); } } Pulldown.prototype.shutdown = function () { disconnectAll( this.close_button ); disconnectAll( this.startup_toggle ); removeElement( this.div ); }