diff --git a/NEWS b/NEWS index 86acac5..3372a02 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ 1.6.12: + * Added a toolbar color button for setting text and background colors. * Added a "start a new discussion" link to each discussion forum page. * Updated Luminotes Server INSTALL file with instructions for setting the http_url configuration setting. diff --git a/controller/Html_cleaner.py b/controller/Html_cleaner.py index b562083..8b89a3a 100644 --- a/controller/Html_cleaner.py +++ b/controller/Html_cleaner.py @@ -19,6 +19,8 @@ class Html_cleaner(HTMLParser): Cleans HTML of any tags not matching a whitelist. """ NOTE_LINK_URL_PATTERN = re.compile( '[^"]*/notebooks/\w+\?[^"]*note_id=\w+', re.IGNORECASE ) + COLOR_RGB_PATTERN = re.compile( "^rgb(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*)$" ) + COLOR_HEX_PATTERN = re.compile( "^#\d{6}$" ) def __init__( self, require_link_target = False ): HTMLParser.__init__( self, AbstractFormatter( NullWriter() ) ) @@ -110,6 +112,7 @@ class Html_cleaner(HTMLParser): 'caption', 'col', 'colgroup', + 'span', ] # A list of tags that require no closing tag. @@ -124,7 +127,8 @@ class Html_cleaner(HTMLParser): 'p': [ 'align' ], 'img': [ 'src', 'alt', 'border', 'title', "class" ], 'table': [ 'cellpadding', 'cellspacing', 'border', 'width', 'height' ], - 'font': [ 'color', 'size', 'face' ], + 'font': [ 'size', 'face', 'color' ], + 'span': [ 'style' ], 'td': [ 'rowspan', 'colspan', 'width', 'height' ], 'th': [ 'rowspan', 'colspan', 'width', 'height' ], } @@ -168,6 +172,12 @@ class Html_cleaner(HTMLParser): else: bt += ' %s=%s' % \ (xssescape(attribute), quoteattr(attrs[attribute])) + if attribute == 'style': + if self.style_is_acceptable( attrs[ attribute ] ): + bt += ' %s="%s"' % (attribute, attrs[attribute]) + else: + bt += ' %s=%s' % \ + (xssescape(attribute), quoteattr(attrs[attribute])) if tag == "a" and \ ( not attrs.get( 'href' ) or not self.NOTE_LINK_URL_PATTERN.search( attrs.get( 'href' ) ) ): if self.require_link_target and not attrs.get( 'target' ): @@ -209,6 +219,29 @@ class Html_cleaner(HTMLParser): return parsed[0] in self.allowed_schemes + def style_is_acceptable(self, style): + pieces = style.split( ";" ) + + for piece in pieces: + piece = piece.strip() + if piece == "": + continue + + param_and_value = piece.split( ":" ) + if len( param_and_value ) != 2: + return False + + ( param, value ) = param_and_value + value = value.strip() + + if param.strip().lower() not in ( "color", "background-color" ): + return False + if not self.COLOR_RGB_PATTERN.search( value ) and \ + not self.COLOR_HEX_PATTERN.search( value ): + return False + + return True + def strip(self, rawstring): """Returns the argument stripped of potentially harmful HTML or JavaScript code""" self.reset() diff --git a/static/css/style.css b/static/css/style.css index e222981..a436d01 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -111,11 +111,11 @@ h1 { } #toolbar .bold_large { - background-position: 440px 0; + background-position: 480px 0; } #toolbar .bold_small { - background-position: 220px -2px; + background-position: 240px -2px; } #toolbar .italic_large { @@ -142,6 +142,14 @@ h1 { background-position: 60px -2px; } +#toolbar .color_large { + background-position: 400px 0; +} + +#toolbar .color_small { + background-position: 200px -2px; +} + #toolbar .font_large { background-position: 360px 0; } @@ -159,11 +167,11 @@ h1 { } #toolbar .insertUnorderedList_large { - background-position: 400px 0; + background-position: 440px 0; } #toolbar .insertUnorderedList_small { - background-position: 200px -2px; + background-position: 220px -2px; } #toolbar .insertOrderedList_large { @@ -848,6 +856,42 @@ h1 { text-decoration: none; } +#color_table { + border-collapse: collapse; + border: 1px solid #000000; + margin-top: 1em; +} + +#color_table td { + border: 1px solid #000000; + width: 1.5em; + height: 1.5em; + text-align: center; + vertical-align: middle; + cursor: pointer; + padding: 0; + margin: 0; +} + +#color_table .color_box { + font-size: 110%; + outline: none; + font-weight: bold; + border: none; + width: 1.5em; + height: 1.5em; + padding: 0; + margin: 0; +} + +.color_box_dark_selected { + color: #ffffff; +} + +.color_box_light_selected { + color: #000000; +} + .selected_mark { vertical-align: top; } @@ -1154,6 +1198,12 @@ h1 { .radio_label { color: #000000; + border: none; + outline: none; + background-color: transparent; + padding: 0; + -moz-user-select: none; + -webkit-user-select: none; } .radio_label:hover { @@ -1161,6 +1211,10 @@ h1 { cursor: pointer; } +.small_button { + font-size: 100%; +} + .hook_action_area { -moz-border-radius: 5px; -webkit-border-radius: 5px; diff --git a/static/images/toolbar/buttons.png b/static/images/toolbar/buttons.png index fc43bc1..a76353a 100644 Binary files a/static/images/toolbar/buttons.png and b/static/images/toolbar/buttons.png differ diff --git a/static/images/toolbar/color_button.xcf b/static/images/toolbar/color_button.xcf new file mode 100644 index 0000000..558beb4 Binary files /dev/null and b/static/images/toolbar/color_button.xcf differ diff --git a/static/images/toolbar/small/buttons.png b/static/images/toolbar/small/buttons.png index 3b90be6..8ee6f63 100644 Binary files a/static/images/toolbar/small/buttons.png and b/static/images/toolbar/small/buttons.png differ diff --git a/static/images/toolbar/small/color_button.xcf b/static/images/toolbar/small/color_button.xcf new file mode 100644 index 0000000..cd41fc2 Binary files /dev/null and b/static/images/toolbar/small/color_button.xcf differ diff --git a/static/js/Editor.js b/static/js/Editor.js index db4eabb..771d7cd 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -485,6 +485,17 @@ Editor.prototype.position_cursor_after = function ( node ) { } } +Editor.prototype.collapse_cursor = function () { + if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox + var selection = this.iframe.contentWindow.getSelection(); + selection.collapseToEnd(); + } else if ( this.document.selection ) { // browsers such as IE + var range = this.document.selection.createRange(); + range.collapse( false ); + range.select(); + } +} + Editor.prototype.connect_handlers = function () { if ( this.document && this.document.body ) { // since the browser may subtly tweak the html when it's inserted, save off the browser's version @@ -551,7 +562,7 @@ Editor.prototype.connect_handlers = function () { } // browsers such as Firefox, but not Opera - if ( !OPERA && this.iframe && this.iframe.contentDocument && this.edit_enabled ) { + if ( GECKO && this.iframe && this.iframe.contentDocument && this.edit_enabled ) { this.exec_command( "styleWithCSS", false ); this.exec_command( "insertbronreturn", true ); } @@ -918,7 +929,9 @@ Editor.prototype.key_released = function ( event ) { // if ctrl keys are released, bail var code = event.key().code; var CTRL = 17; - if ( event.modifier().ctrl || code == CTRL ) + var NONE = 0; + + if ( event.modifier().ctrl || code == CTRL || code == NONE ) return; this.cleanup_html( code ); @@ -1432,7 +1445,7 @@ Editor.prototype.contents = function () { // return true if the given state_name is currently enabled, optionally using a given list of node // names -Editor.prototype.state_enabled = function ( state_name, node_names ) { +Editor.prototype.state_enabled = function ( state_name, node_names, attribute_name ) { if ( !this.edit_enabled ) return false; @@ -1474,8 +1487,23 @@ Editor.prototype.current_node_names = function () { while ( node ) { var name = node.nodeName.toLowerCase(); - if ( name == "strong" ) name = "b"; - if ( name == "em" ) name = "i"; + if ( name == "body" ) + break; + else if ( name == "strong" ) name = "b"; + else if ( name == "em" ) name = "i"; + else if ( name == "font" && node.getAttribute( "face" ) ) + name = "fontface"; + else if ( name == "font" && node.getAttribute( "size" ) ) + name = "fontsize"; + else if ( name == "font" && node.getAttribute( "color" ) ) + name = "color"; + else if ( node.hasAttribute && node.hasAttribute( "style" ) ) { + var color = getStyle( node, "color" ); + var background_color = getStyle( node, "background-color" ); + if ( ( color && color != "transparent" ) || + ( background_color && background_color != "transparent" ) ) + name = "color"; + } if ( name != "a" || node.href ) node_names.push( name ); @@ -1486,6 +1514,74 @@ Editor.prototype.current_node_names = function () { return node_names; } +// return the current effective foreground and background colors as hex code strings +Editor.prototype.current_colors = function () { + var foreground = null; + var background = null; + + if ( !this.edit_enabled || !this.iframe || !this.document ) + return [ null, null ]; + + var node; + if ( window.getSelection ) { // browsers such as Firefox + var selection = this.iframe.contentWindow.getSelection(); + if ( selection.rangeCount > 0 ) + var range = selection.getRangeAt( 0 ); + else + var range = this.document.createRange(); + node = range.endContainer; + } else if ( this.document.selection ) { // browsers such as IE + var range = this.document.selection.createRange(); + node = range.parentElement(); + } + + while ( node ) { + var name = node.nodeName.toLowerCase(); + if ( name == "body" ) + break; + + if ( node.hasAttribute && node.hasAttribute( "style" ) ) { + if ( foreground == null ) { + foreground = getStyle( node, "color" ) + if ( foreground == "transparent" ) + foreground = null; + } + if ( background == null ) { + background = getStyle( node, "background-color" ) + if ( background == "transparent" ) + background = null; + } + } else if ( name == "font" && node.getAttribute( "color" ) ) { + foreground = node.getAttribute( "color" ); + } + + if ( foreground && background ) + break; + + node = node.parentNode; + } + + return [ + foreground ? Color.fromString( foreground ).toHexString() : null, + background ? Color.fromString( background ).toHexString() : null + ]; +} + +Editor.prototype.set_foreground_color = function( color_code ) { + if ( GECKO ) this.exec_command( "styleWithCSS", true ); + this.exec_command( "forecolor", Color.fromString( color_code ).toHexString() ); + if ( GECKO ) this.exec_command( "styleWithCSS", false ); +} + +Editor.prototype.set_background_color = function( color_code ) { + if ( GECKO ) this.exec_command( "styleWithCSS", true ); + if ( MSIE ) + this.exec_command( "backcolor", Color.fromString( color_code ).toHexString() ); + else + this.exec_command( "hilitecolor", Color.fromString( color_code ).toHexString() ); + if ( GECKO ) this.exec_command( "styleWithCSS", false ); +} + Editor.prototype.shutdown = function( event ) { signal( this, "title_changed", this, this.title, null ); this.closed = true; diff --git a/static/js/Wiki.js b/static/js/Wiki.js index f52c15c..0e522a5 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -344,6 +344,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } ); connect( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } ); connect( "strikethrough", "onclick", function ( event ) { self.toggle_button( event, "strikethrough" ); } ); + connect( "color", "onclick", this, "toggle_color_button" ); connect( "font", "onclick", this, "toggle_font_button" ); connect( "title", "onclick", function ( event ) { self.toggle_button( event, "title" ); } ); connect( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } ); @@ -357,6 +358,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri this.make_image_button( "italic" ); this.make_image_button( "underline" ); this.make_image_button( "strikethrough" ); + this.make_image_button( "color" ); this.make_image_button( "font" ); this.make_image_button( "title" ); this.make_image_button( "insertUnorderedList" ); @@ -1498,7 +1500,8 @@ Wiki.prototype.update_toolbar = function() { this.update_button( "italic", "i", node_names ); this.update_button( "underline", "u", node_names ); this.update_button( "strikethrough", "strike", node_names ); - this.update_button( "font", "font", node_names ); + this.update_button( "color", "color", node_names ); + this.update_button( "font", "fontface", node_names ); this.update_button( "title", "h3", node_names ); this.update_button( "insertUnorderedList", "ul", node_names ); this.update_button( "insertOrderedList", "ol", node_names ); @@ -1574,6 +1577,30 @@ Wiki.prototype.toggle_attach_button = function ( event ) { event.stop(); } +Wiki.prototype.toggle_color_button = function ( event ) { + if ( this.focused_editor && this.focused_editor.read_write ) { + this.focused_editor.focus(); + + // if a pulldown is already open, then just close it + var existing_div = getElement( "color_pulldown" ); + + if ( existing_div ) { + this.up_image_button( "color" ); + existing_div.pulldown.shutdown(); + existing_div.pulldown = null; + return; + } + + this.down_image_button( "color" ); + this.clear_messages(); + this.clear_pulldowns(); + + new Color_pulldown( this, this.notebook.object_id, this.invoker, event.target(), this.focused_editor ); + } + + event.stop(); +} + Wiki.prototype.toggle_font_button = function ( event ) { if ( this.focused_editor && this.focused_editor.read_write ) { this.focused_editor.focus(); @@ -4463,6 +4490,265 @@ Suggest_pulldown.prototype.shutdown = function () { } +NAMED_COLORS = [ + [ "#000000", "black" ], + [ "#333333", "steel gray" ], + [ "#696969", "dim gray" ], + [ "#808080", "gray" ], + [ "#a9a9a9", "dark gray" ], + [ "#d3d3d3", "light gray" ], + [ "#f5f5f5", "white smoke" ], + [ "#ffffff", "white" ], + + [ "#800000", "maroon" ], + [ "#8b0000", "dark red" ], + [ "#b22222", "fire brick" ], + [ "#dc143c", "crimson" ], + [ "#ff0000", "red" ], + [ "#ff4500", "orange red" ], + [ "#ff6347", "tomato" ], + [ "#ffa07a", "light salmon" ], + + [ "#8b4513", "saddle brown" ], + [ "#a52a2a", "brown" ], + [ "#a0522d", "sienna" ], + [ "#d2691e", "chocolate" ], + [ "#ff8c00", "dark orange" ], + [ "#ffa500", "orange" ], + [ "#ffd700", "gold" ], + [ "#ffff00", "yellow" ], + + [ "#556b2f", "dark olive green" ], + [ "#006400", "dark green" ], + [ "#008000", "green" ], + [ "#2e8b57", "sea green" ], + [ "#32cd32", "lime green" ], + [ "#00ff00", "lime" ], + [ "#7cfc00", "lawn green" ], + [ "#98fb98", "pale green" ], + + [ "#008b8b", "dark cyan" ], + [ "#20b2aa", "light sea green" ], + [ "#00ced1", "dark turquoise" ], + [ "#66cdaa", "medium aquamarine" ], + [ "#40e0d0", "turquoise" ], + [ "#00ffff", "cyan" ], + [ "#7fffd4", "aquamarine" ], + [ "#afeeee", "pale turquoise" ], + + [ "#191970", "midnight blue" ], + [ "#000080", "navy" ], + [ "#0000ff", "blue" ], + [ "#4169e1", "royal blue" ], + [ "#4682b4", "steel blue" ], + [ "#6495ed", "cornflower blue" ], + [ "#87ceeb", "sky blue" ], + [ "#add8e6", "light blue" ], + + [ "#4b0082", "indigo" ], + [ "#800080", "purple" ], + [ "#9400d3", "dark violet" ], + [ "#8a2be2", "blue violet" ], + [ "#ba55d3", "medium orchid" ], + [ "#da70d6", "orchid" ], + [ "#ee82ee", "violet" ], + [ "#dda0dd", "plum" ], + + [ "#c71585", "medium violet red" ], + [ "#ff1493", "deep pink" ], + [ "#db7093", "pale violet red" ], + [ "#ff69b4", "hot pink" ], + [ "#ffb6c1", "light pink" ], + [ "#ffc0cb", "pink" ], + [ "#ffdab9", "peach puff" ], + [ "#ffe4e1", "misty rose" ], +] + + +function Color_pulldown( wiki, notebook_id, invoker, anchor, editor ) { + anchor.pulldown = this; + this.anchor = anchor; + this.editor = editor; + this.initial_selected_mark = null; + this.selected_color_box = null; + + Pulldown.call( this, wiki, notebook_id, "color_pulldown", anchor ); + + this.invoker = invoker; + + var DEFAULT_FOREGROUND_CODE = "#000000"; + var DEFAULT_BACKGROUND_CODE = "#ffffff"; + var current_colors = editor.current_colors(); + + this.foreground_code = current_colors[ 0 ]; + if ( this.foreground_code == DEFAULT_FOREGROUND_CODE ) + this.foreground_code = null; + + this.background_code = current_colors[ 1 ]; + if ( this.background_code == DEFAULT_BACKGROUND_CODE ) + this.background_code = null; + + var foreground_attributes = { "type": "radio", "id": "foreground_color_radio", "name": "color_type", "value": "foreground" }; + var background_attributes = { "type": "radio", "id": "background_color_radio", "name": "color_type", "value": "background" }; + + if ( this.foreground_code || !this.background_code ) { + foreground_attributes[ "checked" ] = true; + } else { + background_attributes[ "checked" ] = true; + } + + this.foreground_radio = createDOM( "input", foreground_attributes ); + + // using a button here instead of a