├── .gitignore ├── .gitmodules ├── CHANGELOG ├── LICENSE ├── README.markdown ├── Rakefile ├── examples ├── custom_buttons.html ├── custom_toolbar.html ├── editor.css ├── link_selection.html ├── list.html ├── simple.html └── toolbar_subclass.html ├── src ├── wysihat.js └── wysihat │ ├── commands.js │ ├── dom │ ├── bookmark.js │ ├── ierange.js │ ├── range.js │ └── selection.js │ ├── editor.js │ ├── element │ └── sanitize_contents.js │ ├── events │ ├── field_change.js │ ├── frame_loaded.js │ └── selection_change.js │ ├── features.js │ ├── formatting.js │ ├── header.js │ └── toolbar.js └── test ├── editor.html └── unit ├── editor_test.js ├── features_test.js ├── fixtures ├── editor.html ├── range.html └── selection.html ├── formatting_test.js ├── frame_loaded_test.js ├── range_test.js ├── sanitize_contents_test.js ├── selection_test.js └── templates └── default.erb /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | doc/ 3 | test/unit/tmp/* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pdoc"] 2 | path = vendor/pdoc 3 | url = git://github.com/tobie/pdoc.git 4 | [submodule "prototype"] 5 | path = vendor/prototype 6 | url = git://github.com/sstephenson/prototype.git 7 | [submodule "sprockets"] 8 | path = vendor/sprockets 9 | url = git://github.com/sstephenson/sprockets.git 10 | [submodule "unittest_js"] 11 | path = vendor/unittest_js 12 | url = git://github.com/tobie/unittest_js.git 13 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | *0.3* 2 | 3 | * Removed iFrame support and transitioned to contentEditable. This drops 4 | support for Firefox 2. 5 | 6 | *0.2.1* (February 3, 2010) 7 | 8 | * Added Editor#queryValueCommandState for testing custom query commands. 9 | 10 | * Added execCommand delegates for fontSelection, fontSizeSelection, 11 | backgroundColorSelection, backgroundColorSelected, alignSelection, 12 | and alignSelected. 13 | 14 | *0.2* (March 29, 2009) 15 | 16 | * For performance reasons, automatic textarea saving is now disabled by 17 | default. You can still reenable it, but it is recommended to bind the save 18 | to the form's submit button. 19 | 20 | * Requirements for the Toolbar#addButton have changed. The old options will 21 | continue to work, but you no longer have to supply a name or handler if the 22 | names are similar. So the label "Bold" will set the name to "bold" by 23 | convention and invoke the buttonSelection handler on click. 24 | 25 | * The toolbar class has also been refactored to make it easier to subclass. 26 | Subclassing the Toolbar class will be the recommended way to customize the 27 | default behavior. So the options hash from the Toolbar#initialize method has 28 | been deprecated in favor of subclassing. 29 | 30 | * The event system has been revamp so they behave more like their DOM counter 31 | parts. In 0.1, 'wysihat:change' was fired anytime the contents was modified 32 | or the cursor moved. Now the change event is fired only if the contents is 33 | altered. This is emulates normal browser input field's onchange event. If you 34 | still want to track any cursor movements, you can observe 35 | "wysihat:cursormove". This is an extension to the standard set of DOM events. 36 | It is fired anytime the cursor position is changed via mouse clicks or 37 | keyboard navigation. Since typing in the editor causes the cursor to advance 38 | it consequently fires anytime the contents of the editor are modified. 39 | 40 | * To complement the built in commands, selection query helpers have been added. 41 | Example, to check if the selected text is bold, use editor.boldSelected(). 42 | 43 | * A simple command and query registration API has been added to extend or 44 | override built-in commands executed via "execCommand" or "queryCommandState". 45 | You can register commands or query handlers by setting a key/value on 46 | editor.commands or editor.queryCommands. 47 | 48 | *0.1* (October 20, 2008) 49 | 50 | * First public release 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Joshua Peek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | WysiHat 2 | ======= 3 | 4 | #### A WYSIWYG JavaScript framework 5 | 6 | WysiHat is a WYSIWYG JavaScript framework that provides an extensible 7 | foundation to design your own rich text editor. WysiHat stays out of your 8 | way and leaves the UI design to you. 9 | 10 | ### Support platforms 11 | 12 | WysiHat currently supports: 13 | 14 | * Microsoft Internet Explorer for Windows, version 7.0 15 | * Mozilla Firefox 3.0 16 | * Apple Safari 4.0 17 | * Google Chrome 4.0 18 | 19 | ### Dependencies 20 | 21 | * Prototype 1.7 or later (http://prototypejs.org/) 22 | 23 | ## Documentation 24 | 25 | Code is documented inline with PDoc (http://pdoc.org/). 26 | 27 | The generated HTML documentation can be found on the `gh-pages` branch or viewed online at (http://josh.github.com/wysihat/). 28 | 29 | ### Examples 30 | 31 | Several examples can be found under `examples/` to get you started. 32 | 33 | ### Downloading 34 | 35 | Tagged releases will be posted on the GitHub downloads section (http://github.com/josh/wysihat/downloads). 36 | 37 | ### Building from source 38 | 39 | You can build the latest version of WysiHat from source by running 40 | `rake` the root directory. The generated file will be saved to 41 | `dist/wysihat.js`. Ruby and the Rake gem are only required to build 42 | the project from source. It is not required to run the code. 43 | 44 | ## Contributing 45 | 46 | Check out the WysiHat source with 47 | 48 | $ git clone git://github.com/josh/wysihat.git 49 | $ cd wysihat 50 | $ git submodule init 51 | $ git submodule update 52 | 53 | GitHub pull requests are welcome. 54 | 55 | ## License 56 | 57 | WysiHat is released under the MIT license. 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/clean' 3 | require 'rake/rdoctask' 4 | require 'rake/testtask' 5 | 6 | CLEAN.include 'test/unit/tmp' 7 | CLOBBER.include 'dist' 8 | CLOBBER.include 'doc' 9 | 10 | WYSIHAT_ROOT = File.expand_path(File.dirname(__FILE__)) 11 | WYSIHAT_SRC_DIR = File.join(WYSIHAT_ROOT, 'src') 12 | 13 | 14 | # Distribution 15 | 16 | file 'dist/prototype.js' => :sprockets do |t| 17 | prototype_src_dir = "#{WYSIHAT_ROOT}/vendor/prototype/src" 18 | 19 | secretary = Sprockets::Secretary.new( 20 | :root => prototype_src_dir, 21 | :load_path => [prototype_src_dir], 22 | :source_files => ["prototype.js"] 23 | ) 24 | 25 | FileUtils.mkdir_p File.dirname(t.name) 26 | secretary.concatenation.save_to(t.name) 27 | end 28 | 29 | file 'dist/wysihat.js' => Dir['src/**/*'] + [:sprockets] do |t| 30 | secretary = Sprockets::Secretary.new( 31 | :root => WYSIHAT_SRC_DIR, 32 | :load_path => [WYSIHAT_SRC_DIR], 33 | :source_files => ["wysihat.js"] 34 | ) 35 | 36 | FileUtils.mkdir_p File.dirname(t.name) 37 | secretary.concatenation.save_to(t.name) 38 | end 39 | 40 | task :default => :dist 41 | 42 | desc "Builds the distribution." 43 | task :dist => ['dist/prototype.js', 'dist/wysihat.js'] 44 | 45 | 46 | # Documentation 47 | 48 | desc "Builds the documentation." 49 | file 'doc' => Dir['src/**/*'] + [:sprockets, :pdoc] do 50 | require 'tempfile' 51 | 52 | Tempfile.open('pdoc') do |temp| 53 | secretary = Sprockets::Secretary.new( 54 | :root => WYSIHAT_SRC_DIR, 55 | :load_path => [WYSIHAT_SRC_DIR], 56 | :source_files => ["wysihat.js"], 57 | :strip_comments => false 58 | ) 59 | 60 | secretary.concatenation.save_to(temp.path) 61 | PDoc::Runner.new(temp.path, :destination => "#{WYSIHAT_ROOT}/doc").run 62 | end 63 | end 64 | 65 | 66 | # Tests 67 | 68 | file 'test/unit/tmp/tests' => Dir['test/unit/*.js'] + [:unittest_js, :dist] do 69 | FileUtils.mkdir_p File.dirname('test/unit/tmp/tests') 70 | 71 | builder = UnittestJS::Builder::SuiteBuilder.new({ 72 | :input_dir => "#{WYSIHAT_ROOT}/test/unit", 73 | :assets_dir => "#{WYSIHAT_ROOT}/dist" 74 | }) 75 | builder.collect 76 | builder.render 77 | end 78 | 79 | desc "Builds the distribution, runs the JavaScript unit tests and collects their results." 80 | task :test => 'test/unit/tmp/tests' 81 | 82 | 83 | # Vendored libs 84 | 85 | task :sprockets => 'vendor/sprockets/lib/sprockets.rb' do 86 | $:.unshift File.expand_path('vendor/sprockets/lib', WYSIHAT_ROOT) 87 | require 'sprockets' 88 | end 89 | 90 | task :pdoc => 'vendor/pdoc/lib/pdoc.rb' do 91 | $:.unshift File.expand_path('vendor/pdoc/lib', WYSIHAT_ROOT) 92 | require 'pdoc' 93 | end 94 | 95 | task :unittest_js => 'vendor/unittest_js/lib/unittest_js.rb' do 96 | $:.unshift File.expand_path('vendor/unittest_js/lib', WYSIHAT_ROOT) 97 | require 'unittest_js' 98 | end 99 | 100 | file 'vendor/pdoc/lib/pdoc.rb' do 101 | Rake::Task['update_submodules'].invoke 102 | end 103 | 104 | file 'vendor/sprockets/lib/sprockets.rb' do 105 | Rake::Task['update_submodules'].invoke 106 | end 107 | 108 | file 'vendor/unittest_js/lib/unittest_js.rb' do 109 | Rake::Task['update_submodules'].invoke 110 | end 111 | 112 | task :update_submodules do 113 | system "git submodule init" 114 | system "git submodule update" 115 | end 116 | -------------------------------------------------------------------------------- /examples/custom_buttons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 31 | 32 | 33 | 34 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

35 | 36 |

This examples shows how to add custom buttons and actions with the built-in toolbar class.

37 | 38 |
39 |   document.on("dom:loaded", function() {
40 |     var editor = WysiHat.Editor.attach('content');
41 |     var toolbar = new WysiHat.Toolbar(editor);
42 | 
43 |     // The name will be used for the div class and the command to execute
44 |     // The label is the text you see for the button.
45 |     toolbar.addButton({ name: 'bold', label: "Strong" });
46 | 
47 |     // The label is the only required option. If no name is given,
48 |     // the label will be downcased and set to the name.
49 |     toolbar.addButton({ label: "Underline" });
50 | 
51 |     // You can override all the conventions by passing in your own
52 |     // name, label, handler, and query options.
53 |     toolbar.addButton({
54 |       name: 'em',
55 |       label: "Emphasis",
56 |       handler: function(editor) { editor.italicSelection(); },
57 |       query: function(editor) { return editor.italicSelected(); }
58 |     });
59 |   });
60 | 
61 | 62 |
63 | 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /examples/custom_toolbar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 55 | 56 | 57 | 58 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

59 | 60 |

This is a more advanced example that extends the built-in toolbar class. The code is a bit more verbose, however you have full control of the html.

61 | 62 |
 63 |   document.on("dom:loaded", function() {
 64 |     var editor = WysiHat.Editor.attach('content');
 65 | 
 66 |     var boldButton = $$('.toolbar .bold').first();
 67 |     boldButton.on('click', function(event) {
 68 |       editor.boldSelection();
 69 |       Event.stop(event);
 70 |     });
 71 |     editor.on('wysihat:cursormove', function(event) {
 72 |       if (editor.boldSelected())
 73 |         boldButton.addClassName('selected')
 74 |       else
 75 |         boldButton.removeClassName('selected');
 76 |     });
 77 | 
 78 |     var underlineButton = $$('.toolbar .underline').first();
 79 |     underlineButton.on('click', function(event) {
 80 |       editor.underlineSelection();
 81 |       Event.stop(event);
 82 |     });
 83 |     editor.on('wysihat:cursormove', function(event) {
 84 |       if (editor.underlineSelected())
 85 |         underlineButton.addClassName('selected')
 86 |       else
 87 |         underlineButton.removeClassName('selected');
 88 |     });
 89 | 
 90 |     var italicButton = $$('.toolbar .italic').first();
 91 |     italicButton.on('click', function(event) {
 92 |       editor.italicSelection();
 93 |       Event.stop(event);
 94 |     });
 95 |     editor.on('wysihat:cursormove', function(event) {
 96 |       if (editor.italicSelected())
 97 |         italicButton.addClassName('selected')
 98 |       else
 99 |         italicButton.removeClassName('selected');
100 |     });
101 |   });
102 | 
103 | 104 |
105 |
106 | Bold 107 | Underline 108 | Italic 109 |
110 | 111 | 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /examples/editor.css: -------------------------------------------------------------------------------- 1 | .editor { 2 | clear: both; 3 | min-height: 100px; 4 | margin: 5px 0; 5 | padding: 5px; 6 | border: 1px solid #acacac; 7 | outline: none; 8 | font-family: 'Lucida Grande', verdana, arial, sans-serif; 9 | font-size: 12px; 10 | } 11 | 12 | .editor p { 13 | margin: 0; 14 | } 15 | 16 | .editor_toolbar .button { 17 | float: left; 18 | margin: 2px 5px; 19 | } 20 | 21 | .editor_toolbar .selected { 22 | color: red !important; 23 | } 24 | 25 | #error { 26 | font: 18px helvetica, arial, sans-serif; 27 | color: red; 28 | } 29 | -------------------------------------------------------------------------------- /examples/link_selection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 38 | 39 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

40 | 41 |

This example shows you how to create a simple UI for linking and unlinking selections

42 | 43 |
44 |   WysiHat.Commands.promptLinkSelection = function() {
45 |     if (this.linkSelected()) {
46 |       if (confirm("Remove link?"))
47 |         this.unlinkSelection();
48 |     } else {
49 |       var value = prompt("Enter a URL", "http://www.google.com/");
50 |       if (value)
51 |         this.linkSelection(value);
52 |     }
53 |   }
54 | 
55 |   document.on("dom:loaded", function() {
56 |     var editor = WysiHat.Editor.attach('content');
57 |     var toolbar = new WysiHat.Toolbar(editor);
58 | 
59 |     toolbar.addButton({
60 |       label: "Link",
61 |       handler: function(editor) { return editor.promptLinkSelection(); }
62 |     });
63 |   });
64 | 
65 | 66 |
67 | 68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

34 | 35 |

This example shows you how to create a simple UI for inserting (un)ordered Lists

36 | 37 |
38 |   document.on("dom:loaded", function() {
39 |     var editor = WysiHat.Editor.attach('content');
40 |     var toolbar = new WysiHat.Toolbar(editor);
41 | 
42 |     toolbar.addButton({
43 |       label: "Ordered List",
44 |       handler: function(editor) { return editor.toggleOrderedList(); }
45 |     });
46 | 
47 |     toolbar.addButton({
48 |       label: "Unordered List",
49 |       handler: function(editor) { return editor.toggleUnorderedList(); }
50 |     });
51 |   });
52 | 
53 | 54 |
55 | 56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

25 | 26 |

This is a simple example of how easy it is to get up and running with WysiHat.

27 | 28 |
29 |   document.on("dom:loaded", function() {
30 |     // Replaces the textarea 'content' with the wysiwyg editor on load
31 |     var editor = WysiHat.Editor.attach('content');
32 | 
33 |     // Create a simple toolbar bar
34 |     // View the source of this page to see the generated HTML
35 |     var toolbar = new WysiHat.Toolbar(editor);
36 | 
37 |     // Add the basic set of buttons, bold, underline and italic
38 |     toolbar.addButtonSet(WysiHat.Toolbar.ButtonSets.Basic);
39 |   });
40 | 
41 | 42 |
43 | 50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/toolbar_subclass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | 47 | 48 |

Oops, you need to build the package before running this example. It's easy: just run rake in the project's directory.

49 | 50 |

This demonstrates how you can subclass the built in toolbar class and add your own behavior to it.

51 | 52 |
53 |   Toolbar = Class.create(WysiHat.Toolbar, {
54 |     createToolbarElement: function() {
55 |       var toolbar = new Element('div', { 'class': 'toolbar' });
56 |       this.editor.insert({before: toolbar});
57 |       return toolbar;
58 |     },
59 | 
60 |     createButtonElement: function(toolbar, options) {
61 |       var button = Element('a', { 'href': '#' });
62 |       button.update(options.get('label'));
63 |       button.addClassName('button_' + options.get('name'));
64 |       toolbar.appendChild(button);
65 | 
66 |       return button;
67 |     },
68 | 
69 |     updateButtonState: function(element, name, state) {
70 |       if (state)
71 |         element.addClassName('highlight');
72 |       else
73 |         element.removeClassName('highlight');
74 |     }
75 |   });
76 | 
77 |   document.on("dom:loaded", function() {
78 |     var editor = WysiHat.Editor.attach('content');
79 |     var toolbar = new Toolbar(editor);
80 |     toolbar.addButtonSet(WysiHat.Toolbar.ButtonSets.Basic);
81 |   });
82 | 
83 | 84 |
85 | 92 | 93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /src/wysihat.js: -------------------------------------------------------------------------------- 1 | //= require "./wysihat/header" 2 | // 3 | //= require "./wysihat/editor" 4 | //= require "./wysihat/features" 5 | //= require "./wysihat/commands" 6 | // 7 | //= require "./wysihat/dom/ierange" 8 | //= require "./wysihat/dom/range" 9 | //= require "./wysihat/dom/selection" 10 | //= require "./wysihat/dom/bookmark" 11 | // 12 | //= require "./wysihat/element/sanitize_contents" 13 | // 14 | //= require "./wysihat/events/field_change" 15 | //= require "./wysihat/events/frame_loaded" 16 | //= require "./wysihat/events/selection_change" 17 | // 18 | //= require "./wysihat/formatting" 19 | // 20 | //= require "./wysihat/toolbar" 21 | -------------------------------------------------------------------------------- /src/wysihat/commands.js: -------------------------------------------------------------------------------- 1 | //= require "./dom/selection" 2 | //= require "./events/field_change" 3 | 4 | /** section: wysihat 5 | * mixin WysiHat.Commands 6 | * 7 | * Methods will be mixed into the editor element. Most of these 8 | * methods will be used to bind to button clicks or key presses. 9 | * 10 | * var editor = WysiHat.Editor.attach(textarea); 11 | * $('bold_button').on('click', function(event) { 12 | * editor.boldSelection(); 13 | * event.stop(); 14 | * }); 15 | * 16 | * In this example, it is important to stop the click event so you don't 17 | * lose your current selection. 18 | **/ 19 | WysiHat.Commands = (function(window) { 20 | /** 21 | * WysiHat.Commands#boldSelection() -> undefined 22 | * 23 | * Bolds the current selection. 24 | **/ 25 | function boldSelection() { 26 | this.execCommand('bold', false, null); 27 | } 28 | 29 | /** 30 | * WysiHat.Commands#boldSelected() -> boolean 31 | * 32 | * Check if current selection is bold or strong. 33 | **/ 34 | function boldSelected() { 35 | return this.queryCommandState('bold'); 36 | } 37 | 38 | /** 39 | * WysiHat.Commands#underlineSelection() -> undefined 40 | * 41 | * Underlines the current selection. 42 | **/ 43 | function underlineSelection() { 44 | this.execCommand('underline', false, null); 45 | } 46 | 47 | /** 48 | * WysiHat.Commands#underlineSelected() -> boolean 49 | * 50 | * Check if current selection is underlined. 51 | **/ 52 | function underlineSelected() { 53 | return this.queryCommandState('underline'); 54 | } 55 | 56 | /** 57 | * WysiHat.Commands#italicSelection() -> undefined 58 | * 59 | * Italicizes the current selection. 60 | **/ 61 | function italicSelection() { 62 | this.execCommand('italic', false, null); 63 | } 64 | 65 | /** 66 | * WysiHat.Commands#italicSelected() -> boolean 67 | * 68 | * Check if current selection is italic or emphasized. 69 | **/ 70 | function italicSelected() { 71 | return this.queryCommandState('italic'); 72 | } 73 | 74 | /** 75 | * WysiHat.Commands#italicSelection() -> undefined 76 | * 77 | * Strikethroughs the current selection. 78 | **/ 79 | function strikethroughSelection() { 80 | this.execCommand('strikethrough', false, null); 81 | } 82 | 83 | /** 84 | * WysiHat.Commands#indentSelection() -> undefined 85 | * 86 | * Indents the current selection. 87 | **/ 88 | function indentSelection() { 89 | // TODO: Should use feature detection 90 | if (Prototype.Browser.Gecko) { 91 | var selection, range, node, blockquote; 92 | 93 | selection = window.getSelection(); 94 | range = selection.getRangeAt(0); 95 | node = selection.getNode(); 96 | 97 | if (range.collapsed) { 98 | range = document.createRange(); 99 | range.selectNodeContents(node); 100 | selection.removeAllRanges(); 101 | selection.addRange(range); 102 | } 103 | 104 | blockquote = new Element('blockquote'); 105 | range = selection.getRangeAt(0); 106 | range.surroundContents(blockquote); 107 | } else { 108 | this.execCommand('indent', false, null); 109 | } 110 | } 111 | 112 | /** 113 | * WysiHat.Commands#outdentSelection() -> undefined 114 | * 115 | * Outdents the current selection. 116 | **/ 117 | function outdentSelection() { 118 | this.execCommand('outdent', false, null); 119 | } 120 | 121 | /** 122 | * WysiHat.Commands#toggleIndentation() -> undefined 123 | * 124 | * Toggles indentation the current selection. 125 | **/ 126 | function toggleIndentation() { 127 | if (this.indentSelected()) { 128 | this.outdentSelection(); 129 | } else { 130 | this.indentSelection(); 131 | } 132 | } 133 | 134 | /** 135 | * WysiHat.Commands#indentSelected() -> boolean 136 | * 137 | * Check if current selection is indented. 138 | **/ 139 | function indentSelected() { 140 | var node = window.getSelection().getNode(); 141 | return node.match("blockquote, blockquote *"); 142 | } 143 | 144 | /** 145 | * WysiHat.Commands#fontSelection(font) -> undefined 146 | * 147 | * Sets the font for the current selection 148 | **/ 149 | function fontSelection(font) { 150 | this.execCommand('fontname', false, font); 151 | } 152 | 153 | /** 154 | * WysiHat.Commands#fontSizeSelection(fontSize) -> undefined 155 | * - font size (int) : font size for selection 156 | * 157 | * Sets the font size for the current selection 158 | **/ 159 | function fontSizeSelection(fontSize) { 160 | this.execCommand('fontsize', false, fontSize); 161 | } 162 | 163 | /** 164 | * WysiHat.Commands#colorSelection(color) -> undefined 165 | * - color (String): a color name or hexadecimal value 166 | * 167 | * Sets the foreground color of the current selection. 168 | **/ 169 | function colorSelection(color) { 170 | this.execCommand('forecolor', false, color); 171 | } 172 | 173 | /** 174 | * WysiHat.Commands#backgroundColorSelection(color) -> undefined 175 | * - color (string) - a color or hexadecimal value 176 | * 177 | * Sets the background color. Firefox will fill in the background 178 | * color of the entire iframe unless hilitecolor is used. 179 | **/ 180 | function backgroundColorSelection(color) { 181 | if(Prototype.Browser.Gecko) { 182 | this.execCommand('hilitecolor', false, color); 183 | } else { 184 | this.execCommand('backcolor', false, color); 185 | } 186 | } 187 | 188 | /** 189 | * WysiHat.Commands#alignSelection(color) -> undefined 190 | * - alignment (string) - how the text should be aligned (left, center, right) 191 | * 192 | **/ 193 | function alignSelection(alignment) { 194 | this.execCommand('justify' + alignment); 195 | } 196 | 197 | /** 198 | * WysiHat.Commands#backgroundColorSelected() -> alignment 199 | * 200 | * Returns the alignment of the selected text area 201 | **/ 202 | function alignSelected() { 203 | var node = window.getSelection().getNode(); 204 | return Element.getStyle(node, 'textAlign'); 205 | } 206 | 207 | /** 208 | * WysiHat.Commands#linkSelection(url) -> undefined 209 | * - url (String): value for href 210 | * 211 | * Wraps the current selection in a link. 212 | **/ 213 | function linkSelection(url) { 214 | this.execCommand('createLink', false, url); 215 | } 216 | 217 | /** 218 | * WysiHat.Commands#unlinkSelection() -> undefined 219 | * 220 | * Selects the entire link at the cursor and removes it 221 | **/ 222 | function unlinkSelection() { 223 | var node = window.getSelection().getNode(); 224 | if (this.linkSelected()) 225 | window.getSelection().selectNode(node); 226 | 227 | this.execCommand('unlink', false, null); 228 | } 229 | 230 | /** 231 | * WysiHat.Commands#linkSelected() -> boolean 232 | * 233 | * Check if current selection is link. 234 | **/ 235 | function linkSelected() { 236 | var node = window.getSelection().getNode(); 237 | return node ? node.tagName.toUpperCase() == 'A' : false; 238 | } 239 | 240 | /** 241 | * WysiHat.Commands#formatblockSelection(element) -> undefined 242 | * - element (String): the type of element you want to wrap your selection 243 | * with (like 'h1' or 'p'). 244 | * 245 | * Wraps the current selection in a header or paragraph. 246 | **/ 247 | function formatblockSelection(element){ 248 | this.execCommand('formatblock', false, element); 249 | } 250 | 251 | /** 252 | * WysiHat.Commands#toggleOrderedList() -> undefined 253 | * 254 | * Formats current selection as an ordered list. If the selection is empty 255 | * a new list is inserted. 256 | * 257 | * If the selection is already a ordered list, the entire list 258 | * will be toggled. However, toggling the last item of the list 259 | * will only affect that item, not the entire list. 260 | **/ 261 | function toggleOrderedList() { 262 | var selection, node; 263 | 264 | selection = window.getSelection(); 265 | node = selection.getNode(); 266 | 267 | if (this.orderedListSelected() && !node.match("ol li:last-child, ol li:last-child *")) { 268 | selection.selectNode(node.up("ol")); 269 | } else if (this.unorderedListSelected()) { 270 | // Toggle list type 271 | selection.selectNode(node.up("ul")); 272 | } 273 | 274 | this.execCommand('insertorderedlist', false, null); 275 | } 276 | 277 | /** 278 | * WysiHat.Commands#insertOrderedList() -> undefined 279 | * 280 | * Alias for WysiHat.Commands#toggleOrderedList 281 | **/ 282 | function insertOrderedList() { 283 | this.toggleOrderedList(); 284 | } 285 | 286 | /** 287 | * WysiHat.Commands#orderedListSelected() -> boolean 288 | * 289 | * Check if current selection is within an ordered list. 290 | **/ 291 | function orderedListSelected() { 292 | var element = window.getSelection().getNode(); 293 | if (element) return element.match('*[contenteditable=""] ol, *[contenteditable=true] ol, *[contenteditable=""] ol *, *[contenteditable=true] ol *'); 294 | return false; 295 | } 296 | 297 | /** 298 | * WysiHat.Commands#toggleUnorderedList() -> undefined 299 | * 300 | * Formats current selection as an unordered list. If the selection is empty 301 | * a new list is inserted. 302 | * 303 | * If the selection is already a unordered list, the entire list 304 | * will be toggled. However, toggling the last item of the list 305 | * will only affect that item, not the entire list. 306 | **/ 307 | function toggleUnorderedList() { 308 | var selection, node; 309 | 310 | selection = window.getSelection(); 311 | node = selection.getNode(); 312 | 313 | if (this.unorderedListSelected() && !node.match("ul li:last-child, ul li:last-child *")) { 314 | selection.selectNode(node.up("ul")); 315 | } else if (this.orderedListSelected()) { 316 | // Toggle list type 317 | selection.selectNode(node.up("ol")); 318 | } 319 | 320 | this.execCommand('insertunorderedlist', false, null); 321 | } 322 | 323 | /** 324 | * WysiHat.Commands#insertUnorderedList() -> undefined 325 | * 326 | * Alias for WysiHat.Commands#toggleUnorderedList() 327 | **/ 328 | function insertUnorderedList() { 329 | this.toggleUnorderedList(); 330 | } 331 | 332 | /** 333 | * WysiHat.Commands#unorderedListSelected() -> boolean 334 | * 335 | * Check if current selection is within an unordered list. 336 | **/ 337 | function unorderedListSelected() { 338 | var element = window.getSelection().getNode(); 339 | if (element) return element.match('*[contenteditable=""] ul, *[contenteditable=true] ul, *[contenteditable=""] ul *, *[contenteditable=true] ul *'); 340 | return false; 341 | } 342 | 343 | /** 344 | * WysiHat.Commands#insertImage(url) -> undefined 345 | * 346 | * - url (String): value for src 347 | * Insert an image at the insertion point with the given url. 348 | **/ 349 | function insertImage(url) { 350 | this.execCommand('insertImage', false, url); 351 | } 352 | 353 | /** 354 | * WysiHat.Commands#insertHTML(html) -> undefined 355 | * 356 | * - html (String): HTML or plain text 357 | * Insert HTML at the insertion point. 358 | **/ 359 | function insertHTML(html) { 360 | if (Prototype.Browser.IE) { 361 | var range = window.document.selection.createRange(); 362 | range.pasteHTML(html); 363 | range.collapse(false); 364 | range.select(); 365 | } else { 366 | this.execCommand('insertHTML', false, html); 367 | } 368 | } 369 | 370 | /** 371 | * WysiHat.Commands#execCommand(command[, ui = false][, value = null]) -> undefined 372 | * - command (String): Command to execute 373 | * - ui (Boolean): Boolean flag for showing UI. Currenty this not 374 | * implemented by any browser. Just use false. 375 | * - value (String): Value to pass to command 376 | * 377 | * A simple delegation method to the documents execCommand method. 378 | **/ 379 | function execCommand(command, ui, value) { 380 | var handler = this.commands.get(command); 381 | if (handler) { 382 | handler.bind(this)(value); 383 | } else { 384 | try { 385 | window.document.execCommand(command, ui, value); 386 | } catch(e) { return null; } 387 | } 388 | 389 | document.activeElement.fire("field:change"); 390 | } 391 | 392 | /** 393 | * WysiHat.Commands#queryCommandState(state) -> Boolean 394 | * - state (String): bold, italic, underline, etc 395 | * 396 | * A delegation method to the document's queryCommandState method. 397 | * 398 | * Custom states handlers can be added to the queryCommands hash, 399 | * which will be checked before calling the native queryCommandState 400 | * command. 401 | * 402 | * editor.queryCommands.set("link", editor.linkSelected); 403 | **/ 404 | function queryCommandState(state) { 405 | var handler = this.queryCommands.get(state); 406 | if (handler) { 407 | return handler.bind(this)(); 408 | } else { 409 | try { 410 | return window.document.queryCommandState(state); 411 | } catch(e) { return null; } 412 | } 413 | } 414 | 415 | /** 416 | * WysiHat.Commands#getSelectedStyles() -> Hash 417 | * 418 | * Fetches the styles (from the styleSelectors hash) from the current 419 | * selection and returns it as a hash 420 | **/ 421 | function getSelectedStyles() { 422 | var styles = $H({}); 423 | var editor = this; 424 | editor.styleSelectors.each(function(style){ 425 | var node = editor.selection.getNode(); 426 | styles.set(style.first(), Element.getStyle(node, style.last())); 427 | }); 428 | return styles; 429 | } 430 | 431 | return { 432 | boldSelection: boldSelection, 433 | boldSelected: boldSelected, 434 | underlineSelection: underlineSelection, 435 | underlineSelected: underlineSelected, 436 | italicSelection: italicSelection, 437 | italicSelected: italicSelected, 438 | strikethroughSelection: strikethroughSelection, 439 | indentSelection: indentSelection, 440 | outdentSelection: outdentSelection, 441 | toggleIndentation: toggleIndentation, 442 | indentSelected: indentSelected, 443 | fontSelection: fontSelection, 444 | fontSizeSelection: fontSizeSelection, 445 | colorSelection: colorSelection, 446 | backgroundColorSelection: backgroundColorSelection, 447 | alignSelection: alignSelection, 448 | alignSelected: alignSelected, 449 | linkSelection: linkSelection, 450 | unlinkSelection: unlinkSelection, 451 | linkSelected: linkSelected, 452 | formatblockSelection: formatblockSelection, 453 | toggleOrderedList: toggleOrderedList, 454 | insertOrderedList: insertOrderedList, 455 | orderedListSelected: orderedListSelected, 456 | toggleUnorderedList: toggleUnorderedList, 457 | insertUnorderedList: insertUnorderedList, 458 | unorderedListSelected: unorderedListSelected, 459 | insertImage: insertImage, 460 | insertHTML: insertHTML, 461 | execCommand: execCommand, 462 | queryCommandState: queryCommandState, 463 | getSelectedStyles: getSelectedStyles, 464 | 465 | commands: $H({}), 466 | 467 | queryCommands: $H({ 468 | link: linkSelected, 469 | orderedlist: orderedListSelected, 470 | unorderedlist: unorderedListSelected 471 | }), 472 | 473 | styleSelectors: $H({ 474 | fontname: 'fontFamily', 475 | fontsize: 'fontSize', 476 | forecolor: 'color', 477 | hilitecolor: 'backgroundColor', 478 | backcolor: 'backgroundColor' 479 | }) 480 | }; 481 | })(window); 482 | -------------------------------------------------------------------------------- /src/wysihat/dom/bookmark.js: -------------------------------------------------------------------------------- 1 | //= require "./selection" 2 | 3 | if (Prototype.Browser.IE) { 4 | Object.extend(Selection.prototype, (function() { 5 | function setBookmark() { 6 | var bookmark = $('bookmark'); 7 | if (bookmark) bookmark.remove(); 8 | 9 | bookmark = new Element('span', { 'id': 'bookmark' }).update(" "); 10 | var parent = new Element('div'); 11 | parent.appendChild(bookmark); 12 | 13 | var range = this._document.selection.createRange(); 14 | range.collapse(); 15 | range.pasteHTML(parent.innerHTML); 16 | } 17 | 18 | function moveToBookmark() { 19 | var bookmark = $('bookmark'); 20 | if (!bookmark) return; 21 | 22 | var range = this._document.selection.createRange(); 23 | range.moveToElementText(bookmark); 24 | range.collapse(); 25 | range.select(); 26 | 27 | bookmark.remove(); 28 | } 29 | 30 | return { 31 | setBookmark: setBookmark, 32 | moveToBookmark: moveToBookmark 33 | } 34 | })()); 35 | } else { 36 | Object.extend(Selection.prototype, (function() { 37 | function setBookmark() { 38 | var bookmark = $('bookmark'); 39 | if (bookmark) bookmark.remove(); 40 | 41 | bookmark = new Element('span', { 'id': 'bookmark' }).update(" "); 42 | this.getRangeAt(0).insertNode(bookmark); 43 | } 44 | 45 | function moveToBookmark() { 46 | var bookmark = $('bookmark'); 47 | if (!bookmark) return; 48 | 49 | var range = document.createRange(); 50 | range.setStartBefore(bookmark); 51 | this.removeAllRanges(); 52 | this.addRange(range); 53 | 54 | bookmark.remove(); 55 | } 56 | 57 | return { 58 | setBookmark: setBookmark, 59 | moveToBookmark: moveToBookmark 60 | } 61 | })()); 62 | } 63 | -------------------------------------------------------------------------------- /src/wysihat/dom/ierange.js: -------------------------------------------------------------------------------- 1 | /* IE Selection and Range classes 2 | * 3 | * Original created by Tim Cameron Ryan 4 | * http://github.com/timcameronryan/IERange 5 | * Copyright (c) 2009 Tim Cameron Ryan 6 | * Released under the MIT/X License 7 | * 8 | * Modified by Joshua Peek 9 | */ 10 | if (!window.getSelection) { 11 | // TODO: Move this object into a closure 12 | var DOMUtils = { 13 | isDataNode: function(node) { 14 | try { 15 | return node && node.nodeValue !== null && node.data !== null; 16 | } catch (e) { 17 | return false; 18 | } 19 | }, 20 | isAncestorOf: function(parent, node) { 21 | if (!parent) return false; 22 | return !DOMUtils.isDataNode(parent) && 23 | (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) || 24 | node.parentNode == parent); 25 | }, 26 | isAncestorOrSelf: function(root, node) { 27 | return DOMUtils.isAncestorOf(root, node) || root == node; 28 | }, 29 | findClosestAncestor: function(root, node) { 30 | if (DOMUtils.isAncestorOf(root, node)) 31 | while (node && node.parentNode != root) 32 | node = node.parentNode; 33 | return node; 34 | }, 35 | getNodeLength: function(node) { 36 | return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length; 37 | }, 38 | splitDataNode: function(node, offset) { 39 | if (!DOMUtils.isDataNode(node)) 40 | return false; 41 | var newNode = node.cloneNode(false); 42 | node.deleteData(offset, node.length); 43 | newNode.deleteData(0, offset); 44 | node.parentNode.insertBefore(newNode, node.nextSibling); 45 | } 46 | }; 47 | 48 | window.Range = (function() { 49 | function Range(document) { 50 | // save document parameter 51 | this._document = document; 52 | 53 | // initialize range 54 | this.startContainer = this.endContainer = document.body; 55 | this.endOffset = DOMUtils.getNodeLength(document.body); 56 | } 57 | Range.START_TO_START = 0; 58 | Range.START_TO_END = 1; 59 | Range.END_TO_END = 2; 60 | Range.END_TO_START = 3; 61 | 62 | function findChildPosition(node) { 63 | for (var i = 0; node = node.previousSibling; i++) 64 | continue; 65 | return i; 66 | } 67 | 68 | Range.prototype = { 69 | startContainer: null, 70 | startOffset: 0, 71 | endContainer: null, 72 | endOffset: 0, 73 | commonAncestorContainer: null, 74 | collapsed: false, 75 | _document: null, 76 | 77 | _toTextRange: function() { 78 | function adoptEndPoint(textRange, domRange, bStart) { 79 | // find anchor node and offset 80 | var container = domRange[bStart ? 'startContainer' : 'endContainer']; 81 | var offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0; 82 | var anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset]; 83 | var anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container; 84 | 85 | // visible data nodes need a text offset 86 | if (container.nodeType == 3 || container.nodeType == 4) 87 | textOffset = offset; 88 | 89 | // create a cursor element node to position range (since we can't select text nodes) 90 | var cursorNode = domRange._document.createElement('a'); 91 | if (anchorNode) 92 | anchorParent.insertBefore(cursorNode, anchorNode); 93 | else 94 | anchorParent.appendChild(cursorNode); 95 | var cursor = domRange._document.body.createTextRange(); 96 | cursor.moveToElementText(cursorNode); 97 | cursorNode.parentNode.removeChild(cursorNode); 98 | 99 | // move range 100 | textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor); 101 | textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset); 102 | } 103 | 104 | // return an IE text range 105 | var textRange = this._document.body.createTextRange(); 106 | adoptEndPoint(textRange, this, true); 107 | adoptEndPoint(textRange, this, false); 108 | return textRange; 109 | }, 110 | 111 | _refreshProperties: function() { 112 | // collapsed attribute 113 | this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset); 114 | // find common ancestor 115 | var node = this.startContainer; 116 | while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer)) 117 | node = node.parentNode; 118 | this.commonAncestorContainer = node; 119 | }, 120 | 121 | setStart: function(container, offset) { 122 | this.startContainer = container; 123 | this.startOffset = offset; 124 | this._refreshProperties(); 125 | }, 126 | setEnd: function(container, offset) { 127 | this.endContainer = container; 128 | this.endOffset = offset; 129 | this._refreshProperties(); 130 | }, 131 | setStartBefore: function(refNode) { 132 | // set start to beore this node 133 | this.setStart(refNode.parentNode, findChildPosition(refNode)); 134 | }, 135 | setStartAfter: function(refNode) { 136 | // select next sibling 137 | this.setStart(refNode.parentNode, findChildPosition(refNode) + 1); 138 | }, 139 | setEndBefore: function(refNode) { 140 | // set end to beore this node 141 | this.setEnd(refNode.parentNode, findChildPosition(refNode)); 142 | }, 143 | setEndAfter: function(refNode) { 144 | // select next sibling 145 | this.setEnd(refNode.parentNode, findChildPosition(refNode) + 1); 146 | }, 147 | selectNode: function(refNode) { 148 | this.setStartBefore(refNode); 149 | this.setEndAfter(refNode); 150 | }, 151 | selectNodeContents: function(refNode) { 152 | this.setStart(refNode, 0); 153 | this.setEnd(refNode, DOMUtils.getNodeLength(refNode)); 154 | }, 155 | collapse: function(toStart) { 156 | if (toStart) 157 | this.setEnd(this.startContainer, this.startOffset); 158 | else 159 | this.setStart(this.endContainer, this.endOffset); 160 | }, 161 | 162 | cloneContents: function() { 163 | // clone subtree 164 | return (function cloneSubtree(iterator) { 165 | for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { 166 | node = node.cloneNode(!iterator.hasPartialSubtree()); 167 | if (iterator.hasPartialSubtree()) 168 | node.appendChild(cloneSubtree(iterator.getSubtreeIterator())); 169 | frag.appendChild(node); 170 | } 171 | return frag; 172 | })(new RangeIterator(this)); 173 | }, 174 | extractContents: function() { 175 | // cache range and move anchor points 176 | var range = this.cloneRange(); 177 | if (this.startContainer != this.commonAncestorContainer) 178 | this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); 179 | this.collapse(true); 180 | // extract range 181 | return (function extractSubtree(iterator) { 182 | for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { 183 | iterator.hasPartialSubtree() ? node = node.cloneNode(false) : iterator.remove(); 184 | if (iterator.hasPartialSubtree()) 185 | node.appendChild(extractSubtree(iterator.getSubtreeIterator())); 186 | frag.appendChild(node); 187 | } 188 | return frag; 189 | })(new RangeIterator(range)); 190 | }, 191 | deleteContents: function() { 192 | // cache range and move anchor points 193 | var range = this.cloneRange(); 194 | if (this.startContainer != this.commonAncestorContainer) 195 | this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); 196 | this.collapse(true); 197 | // delete range 198 | (function deleteSubtree(iterator) { 199 | while (iterator.next()) 200 | iterator.hasPartialSubtree() ? deleteSubtree(iterator.getSubtreeIterator()) : iterator.remove(); 201 | })(new RangeIterator(range)); 202 | }, 203 | insertNode: function(newNode) { 204 | // set original anchor and insert node 205 | if (DOMUtils.isDataNode(this.startContainer)) { 206 | DOMUtils.splitDataNode(this.startContainer, this.startOffset); 207 | this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling); 208 | } else { 209 | var offsetNode = this.startContainer.childNodes[this.startOffset]; 210 | if (offsetNode) { 211 | this.startContainer.insertBefore(newNode, offsetNode); 212 | } else { 213 | this.startContainer.appendChild(newNode); 214 | } 215 | } 216 | // resync start anchor 217 | this.setStart(this.startContainer, this.startOffset); 218 | }, 219 | surroundContents: function(newNode) { 220 | // extract and surround contents 221 | var content = this.extractContents(); 222 | this.insertNode(newNode); 223 | newNode.appendChild(content); 224 | this.selectNode(newNode); 225 | }, 226 | 227 | compareBoundaryPoints: function(how, sourceRange) { 228 | // get anchors 229 | var containerA, offsetA, containerB, offsetB; 230 | switch (how) { 231 | case Range.START_TO_START: 232 | case Range.START_TO_END: 233 | containerA = this.startContainer; 234 | offsetA = this.startOffset; 235 | break; 236 | case Range.END_TO_END: 237 | case Range.END_TO_START: 238 | containerA = this.endContainer; 239 | offsetA = this.endOffset; 240 | break; 241 | } 242 | switch (how) { 243 | case Range.START_TO_START: 244 | case Range.END_TO_START: 245 | containerB = sourceRange.startContainer; 246 | offsetB = sourceRange.startOffset; 247 | break; 248 | case Range.START_TO_END: 249 | case Range.END_TO_END: 250 | containerB = sourceRange.endContainer; 251 | offsetB = sourceRange.endOffset; 252 | break; 253 | } 254 | 255 | // compare 256 | return containerA.sourceIndex < containerB.sourceIndex ? -1 : 257 | containerA.sourceIndex == containerB.sourceIndex ? 258 | offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1 259 | : 1; 260 | }, 261 | cloneRange: function() { 262 | // return cloned range 263 | var range = new Range(this._document); 264 | range.setStart(this.startContainer, this.startOffset); 265 | range.setEnd(this.endContainer, this.endOffset); 266 | return range; 267 | }, 268 | detach: function() { 269 | }, 270 | toString: function() { 271 | return this._toTextRange().text; 272 | }, 273 | createContextualFragment: function(tagString) { 274 | // parse the tag string in a context node 275 | var content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false); 276 | content.innerHTML = tagString; 277 | // return a document fragment from the created node 278 | for (var fragment = this._document.createDocumentFragment(); content.firstChild; ) 279 | fragment.appendChild(content.firstChild); 280 | return fragment; 281 | } 282 | }; 283 | 284 | function RangeIterator(range) { 285 | this.range = range; 286 | if (range.collapsed) 287 | return; 288 | 289 | // get anchors 290 | var root = range.commonAncestorContainer; 291 | this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ? 292 | range.startContainer.childNodes[range.startOffset] : 293 | DOMUtils.findClosestAncestor(root, range.startContainer); 294 | this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ? 295 | range.endContainer.childNodes[range.endOffset] : 296 | DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling; 297 | } 298 | 299 | RangeIterator.prototype = { 300 | range: null, 301 | _current: null, 302 | _next: null, 303 | _end: null, 304 | 305 | hasNext: function() { 306 | return !!this._next; 307 | }, 308 | next: function() { 309 | // move to next node 310 | var current = this._current = this._next; 311 | this._next = this._current && this._current.nextSibling != this._end ? 312 | this._current.nextSibling : null; 313 | 314 | // check for partial text nodes 315 | if (DOMUtils.isDataNode(this._current)) { 316 | if (this.range.endContainer == this._current) 317 | (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset); 318 | if (this.range.startContainer == this._current) 319 | (current = current.cloneNode(true)).deleteData(0, this.range.startOffset); 320 | } 321 | return current; 322 | }, 323 | remove: function() { 324 | // check for partial text nodes 325 | if (DOMUtils.isDataNode(this._current) && 326 | (this.range.startContainer == this._current || this.range.endContainer == this._current)) { 327 | var start = this.range.startContainer == this._current ? this.range.startOffset : 0; 328 | var end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length; 329 | this._current.deleteData(start, end - start); 330 | } else 331 | this._current.parentNode.removeChild(this._current); 332 | }, 333 | hasPartialSubtree: function() { 334 | // check if this node be partially selected 335 | return !DOMUtils.isDataNode(this._current) && 336 | (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) || 337 | DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)); 338 | }, 339 | getSubtreeIterator: function() { 340 | // create a new range 341 | var subRange = new Range(this.range._document); 342 | subRange.selectNodeContents(this._current); 343 | // handle anchor points 344 | if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer)) 345 | subRange.setStart(this.range.startContainer, this.range.startOffset); 346 | if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)) 347 | subRange.setEnd(this.range.endContainer, this.range.endOffset); 348 | // return iterator 349 | return new RangeIterator(subRange); 350 | } 351 | }; 352 | 353 | return Range; 354 | })(); 355 | 356 | window.Range._fromTextRange = function(textRange, document) { 357 | function adoptBoundary(domRange, textRange, bStart) { 358 | // iterate backwards through parent element to find anchor location 359 | var cursorNode = document.createElement('a'), cursor = textRange.duplicate(); 360 | cursor.collapse(bStart); 361 | var parent = cursor.parentElement(); 362 | do { 363 | parent.insertBefore(cursorNode, cursorNode.previousSibling); 364 | cursor.moveToElementText(cursorNode); 365 | } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling); 366 | 367 | // when we exceed or meet the cursor, we've found the node 368 | if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) { 369 | // data node 370 | cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange); 371 | domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length); 372 | } else { 373 | // element 374 | domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode); 375 | } 376 | cursorNode.parentNode.removeChild(cursorNode); 377 | } 378 | 379 | // return a DOM range 380 | var domRange = new Range(document); 381 | adoptBoundary(domRange, textRange, true); 382 | adoptBoundary(domRange, textRange, false); 383 | return domRange; 384 | } 385 | 386 | document.createRange = function() { 387 | return new Range(document); 388 | }; 389 | 390 | window.Selection = (function() { 391 | function Selection(document) { 392 | this._document = document; 393 | 394 | var selection = this; 395 | document.attachEvent('onselectionchange', function() { 396 | selection._selectionChangeHandler(); 397 | }); 398 | } 399 | 400 | Selection.prototype = { 401 | rangeCount: 0, 402 | _document: null, 403 | 404 | _selectionChangeHandler: function() { 405 | this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0; 406 | }, 407 | _selectionExists: function(textRange) { 408 | return textRange.compareEndPoints('StartToEnd', textRange) != 0 || 409 | textRange.parentElement().isContentEditable; 410 | }, 411 | addRange: function(range) { 412 | var selection = this._document.selection.createRange(), textRange = range._toTextRange(); 413 | if (!this._selectionExists(selection)) { 414 | textRange.select(); 415 | } else { 416 | // only modify range if it intersects with current range 417 | if (textRange.compareEndPoints('StartToStart', selection) == -1) 418 | if (textRange.compareEndPoints('StartToEnd', selection) > -1 && 419 | textRange.compareEndPoints('EndToEnd', selection) == -1) 420 | selection.setEndPoint('StartToStart', textRange); 421 | else 422 | if (textRange.compareEndPoints('EndToStart', selection) < 1 && 423 | textRange.compareEndPoints('EndToEnd', selection) > -1) 424 | selection.setEndPoint('EndToEnd', textRange); 425 | selection.select(); 426 | } 427 | }, 428 | removeAllRanges: function() { 429 | this._document.selection.empty(); 430 | }, 431 | getRangeAt: function(index) { 432 | var textRange = this._document.selection.createRange(); 433 | if (this._selectionExists(textRange)) 434 | return Range._fromTextRange(textRange, this._document); 435 | return null; 436 | }, 437 | toString: function() { 438 | return this._document.selection.createRange().text; 439 | } 440 | }; 441 | 442 | return Selection; 443 | })(); 444 | 445 | window.getSelection = (function() { 446 | var selection = new Selection(document); 447 | return function() { return selection; }; 448 | })(); 449 | 450 | window.getSelection.custom = true; 451 | } 452 | -------------------------------------------------------------------------------- /src/wysihat/dom/range.js: -------------------------------------------------------------------------------- 1 | //= require "./ierange" 2 | 3 | Object.extend(Range.prototype, (function() { 4 | function beforeRange(range) { 5 | if (!range || !range.compareBoundaryPoints) return false; 6 | return (this.compareBoundaryPoints(this.START_TO_START, range) == -1 && 7 | this.compareBoundaryPoints(this.START_TO_END, range) == -1 && 8 | this.compareBoundaryPoints(this.END_TO_END, range) == -1 && 9 | this.compareBoundaryPoints(this.END_TO_START, range) == -1); 10 | } 11 | 12 | function afterRange(range) { 13 | if (!range || !range.compareBoundaryPoints) return false; 14 | return (this.compareBoundaryPoints(this.START_TO_START, range) == 1 && 15 | this.compareBoundaryPoints(this.START_TO_END, range) == 1 && 16 | this.compareBoundaryPoints(this.END_TO_END, range) == 1 && 17 | this.compareBoundaryPoints(this.END_TO_START, range) == 1); 18 | } 19 | 20 | function betweenRange(range) { 21 | if (!range || !range.compareBoundaryPoints) return false; 22 | return !(this.beforeRange(range) || this.afterRange(range)); 23 | } 24 | 25 | function equalRange(range) { 26 | if (!range || !range.compareBoundaryPoints) return false; 27 | return (this.compareBoundaryPoints(this.START_TO_START, range) == 0 && 28 | this.compareBoundaryPoints(this.START_TO_END, range) == 1 && 29 | this.compareBoundaryPoints(this.END_TO_END, range) == 0 && 30 | this.compareBoundaryPoints(this.END_TO_START, range) == -1); 31 | } 32 | 33 | function getNode() { 34 | var parent = this.commonAncestorContainer; 35 | 36 | while (parent.nodeType == Node.TEXT_NODE) 37 | parent = parent.parentNode; 38 | 39 | var child = parent.childElements().detect(function(child) { 40 | var range = document.createRange(); 41 | range.selectNodeContents(child); 42 | return this.betweenRange(range); 43 | }.bind(this)); 44 | 45 | return $(child || parent); 46 | } 47 | 48 | return { 49 | beforeRange: beforeRange, 50 | afterRange: afterRange, 51 | betweenRange: betweenRange, 52 | equalRange: equalRange, 53 | getNode: getNode 54 | }; 55 | })()); 56 | -------------------------------------------------------------------------------- /src/wysihat/dom/selection.js: -------------------------------------------------------------------------------- 1 | //= require "./ierange" 2 | //= require "./range" 3 | 4 | if (window.getSelection.custom) { 5 | Object.extend(Selection.prototype, (function() { 6 | // TODO: More robust getNode 7 | function getNode() { 8 | var range = this._document.selection.createRange(); 9 | return $(range.parentElement()); 10 | } 11 | 12 | // TODO: IE selectNode should work with range.selectNode 13 | function selectNode(element) { 14 | var range = this._document.body.createTextRange(); 15 | range.moveToElementText(element); 16 | range.select(); 17 | } 18 | 19 | return { 20 | getNode: getNode, 21 | selectNode: selectNode 22 | } 23 | })()); 24 | } else { 25 | // WebKit does not have a public Selection prototype 26 | if (typeof Selection == 'undefined') { 27 | var Selection = {} 28 | Selection.prototype = window.getSelection().__proto__; 29 | } 30 | 31 | Object.extend(Selection.prototype, (function() { 32 | function getNode() { 33 | if (this.rangeCount > 0) 34 | return this.getRangeAt(0).getNode(); 35 | else 36 | return null; 37 | } 38 | 39 | function selectNode(element) { 40 | var range = document.createRange(); 41 | range.selectNode(element); 42 | this.removeAllRanges(); 43 | this.addRange(range); 44 | } 45 | 46 | return { 47 | getNode: getNode, 48 | selectNode: selectNode 49 | } 50 | })()); 51 | } 52 | -------------------------------------------------------------------------------- /src/wysihat/editor.js: -------------------------------------------------------------------------------- 1 | /** section: wysihat 2 | * WysiHat.Editor 3 | **/ 4 | WysiHat.Editor = { 5 | /** section: wysihat 6 | * WysiHat.Editor.attach(textarea) -> undefined 7 | * - textarea (String | Element): an id or DOM node of the textarea that 8 | * you want to convert to rich text. 9 | * 10 | * Creates a new editor for the textarea. 11 | **/ 12 | attach: function(textarea) { 13 | var editArea; 14 | 15 | textarea = $(textarea); 16 | 17 | var id = textarea.id + '_editor'; 18 | if (editArea = $(id)) return editArea; 19 | 20 | editArea = new Element('div', { 21 | 'id': id, 22 | 'class': 'editor', 23 | 'contentEditable': 'true' 24 | }); 25 | 26 | editArea.update(WysiHat.Formatting.getBrowserMarkupFrom(textarea.value)); 27 | 28 | Object.extend(editArea, WysiHat.Commands); 29 | 30 | textarea.insert({before: editArea}); 31 | textarea.hide(); 32 | 33 | // WysiHat.BrowserFeatures.run() 34 | 35 | return editArea; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/wysihat/element/sanitize_contents.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function cloneWithAllowedAttributes(element, allowedAttributes) { 3 | var result = new Element(element.tagName), length = allowedAttributes.length, i; 4 | element = $(element); 5 | 6 | for (i = 0; i < allowedAttributes.length; i++) { 7 | attribute = allowedAttributes[i]; 8 | if (element.hasAttribute(attribute)) { 9 | result.writeAttribute(attribute, element.readAttribute(attribute)); 10 | } 11 | } 12 | 13 | return result; 14 | } 15 | 16 | function withEachChildNodeOf(element, callback) { 17 | var nodes = $A(element.childNodes), length = nodes.length, i; 18 | for (i = 0; i < length; i++) callback(nodes[i]); 19 | } 20 | 21 | function sanitizeNode(node, tagsToRemove, tagsToAllow, tagsToSkip) { 22 | var parentNode = node.parentNode; 23 | 24 | switch (node.nodeType) { 25 | case Node.ELEMENT_NODE: 26 | var tagName = node.tagName.toLowerCase(); 27 | 28 | if (tagsToSkip) { 29 | var newNode = node.cloneNode(false); 30 | withEachChildNodeOf(node, function(childNode) { 31 | newNode.appendChild(childNode); 32 | sanitizeNode(childNode, tagsToRemove, tagsToAllow, tagsToSkip); 33 | }); 34 | parentNode.insertBefore(newNode, node); 35 | 36 | } else if (tagName in tagsToAllow) { 37 | var newNode = cloneWithAllowedAttributes(node, tagsToAllow[tagName]); 38 | withEachChildNodeOf(node, function(childNode) { 39 | newNode.appendChild(childNode); 40 | sanitizeNode(childNode, tagsToRemove, tagsToAllow, tagsToSkip); 41 | }); 42 | parentNode.insertBefore(newNode, node); 43 | 44 | } else if (!(tagName in tagsToRemove)) { 45 | withEachChildNodeOf(node, function(childNode) { 46 | parentNode.insertBefore(childNode, node); 47 | sanitizeNode(childNode, tagsToRemove, tagsToAllow, tagsToSkip); 48 | }); 49 | } 50 | 51 | case Node.COMMENT_NODE: 52 | parentNode.removeChild(node); 53 | } 54 | } 55 | 56 | Element.addMethods({ 57 | sanitizeContents: function(element, options) { 58 | element = $(element); 59 | 60 | var tagsToRemove = {}; 61 | (options.remove || "").split(",").each(function(tagName) { 62 | tagsToRemove[tagName.strip()] = true; 63 | }); 64 | 65 | var tagsToAllow = {}; 66 | (options.allow || "").split(",").each(function(selector) { 67 | var parts = selector.strip().split(/[\[\]]/); 68 | var tagName = parts[0], allowedAttributes = parts.slice(1).grep(/./); 69 | tagsToAllow[tagName] = allowedAttributes; 70 | }); 71 | 72 | var tagsToSkip = options.skip; 73 | 74 | withEachChildNodeOf(element, function(childNode) { 75 | sanitizeNode(childNode, tagsToRemove, tagsToAllow, tagsToSkip); 76 | }); 77 | 78 | return element; 79 | } 80 | }); 81 | })(); 82 | -------------------------------------------------------------------------------- /src/wysihat/events/field_change.js: -------------------------------------------------------------------------------- 1 | document.on("dom:loaded", function() { 2 | function fieldChangeHandler(event, element) { 3 | var value; 4 | 5 | if (element.contentEditable == 'true') 6 | value = element.innerHTML; 7 | else if (element.getValue) 8 | value = element.getValue(); 9 | 10 | if (value && element.previousValue != value) { 11 | element.fire("field:change"); 12 | element.previousValue = value; 13 | } 14 | } 15 | 16 | $(document.body).on("keyup", 'input,textarea,*[contenteditable=""],*[contenteditable=true]', fieldChangeHandler); 17 | }); 18 | -------------------------------------------------------------------------------- /src/wysihat/events/frame_loaded.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function onReadyStateComplete(document, callback) { 3 | var handler; 4 | 5 | function checkReadyState() { 6 | if (document.readyState === 'complete') { 7 | if (handler) handler.stop(); 8 | callback(); 9 | return true; 10 | } else { 11 | return false; 12 | } 13 | } 14 | 15 | handler = Element.on(document, 'readystatechange', checkReadyState); 16 | checkReadyState(); 17 | } 18 | 19 | function observeFrameContentLoaded(element) { 20 | element = $(element); 21 | 22 | var loaded, contentLoadedHandler; 23 | 24 | loaded = false; 25 | function fireFrameLoaded() { 26 | if (loaded) return; 27 | 28 | loaded = true; 29 | if (contentLoadedHandler) contentLoadedHandler.stop(); 30 | element.fire('frame:loaded'); 31 | } 32 | 33 | if (window.addEventListener) { 34 | contentLoadedHandler = document.on("DOMFrameContentLoaded", function(event) { 35 | if (element == event.element()) 36 | fireFrameLoaded(); 37 | }); 38 | } 39 | 40 | element.on('load', function() { 41 | var frameDocument; 42 | 43 | if (typeof element.contentDocument !== 'undefined') { 44 | frameDocument = element.contentDocument; 45 | } else if (typeof element.contentWindow !== 'undefined' && typeof element.contentWindow.document !== 'undefined') { 46 | frameDocument = element.contentWindow.document; 47 | } 48 | 49 | onReadyStateComplete(frameDocument, fireFrameLoaded); 50 | }); 51 | 52 | return element; 53 | } 54 | 55 | function onFrameLoaded(element, callback) { 56 | element.on('frame:loaded', callback); 57 | element.observeFrameContentLoaded(); 58 | } 59 | 60 | Element.addMethods({ 61 | observeFrameContentLoaded: observeFrameContentLoaded, 62 | onFrameLoaded: onFrameLoaded 63 | }); 64 | })(); 65 | -------------------------------------------------------------------------------- /src/wysihat/events/selection_change.js: -------------------------------------------------------------------------------- 1 | document.on("dom:loaded", function() { 2 | if ('selection' in document && 'onselectionchange' in document) { 3 | var selectionChangeHandler = function() { 4 | var range = document.selection.createRange(); 5 | var element = range.parentElement(); 6 | $(element).fire("selection:change"); 7 | } 8 | 9 | document.on("selectionchange", selectionChangeHandler); 10 | } else { 11 | var previousRange; 12 | 13 | var selectionChangeHandler = function() { 14 | var element = document.activeElement; 15 | var elementTagName = element.tagName.toLowerCase(); 16 | 17 | if (elementTagName == "textarea" || elementTagName == "input") { 18 | previousRange = null; 19 | $(element).fire("selection:change"); 20 | } else { 21 | var selection = window.getSelection(); 22 | if (selection.rangeCount < 1) return; 23 | 24 | var range = selection.getRangeAt(0); 25 | if (range && range.equalRange(previousRange)) return; 26 | previousRange = range; 27 | 28 | element = range.commonAncestorContainer; 29 | while (element.nodeType == Node.TEXT_NODE) 30 | element = element.parentNode; 31 | 32 | $(element).fire("selection:change"); 33 | } 34 | }; 35 | 36 | document.on("mouseup", selectionChangeHandler); 37 | document.on("keyup", selectionChangeHandler); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/wysihat/features.js: -------------------------------------------------------------------------------- 1 | WysiHat.BrowserFeatures = (function() { 2 | function createTmpIframe(callback) { 3 | var frame, frameDocument; 4 | 5 | frame = new Element('iframe'); 6 | frame.setStyle({ 7 | position: 'absolute', 8 | left: '-1000px' 9 | }); 10 | 11 | frame.onFrameLoaded(function() { 12 | if (typeof frame.contentDocument !== 'undefined') { 13 | frameDocument = frame.contentDocument; 14 | } else if (typeof frame.contentWindow !== 'undefined' && typeof frame.contentWindow.document !== 'undefined') { 15 | frameDocument = frame.contentWindow.document; 16 | } 17 | 18 | frameDocument.designMode = 'on'; 19 | 20 | callback(frameDocument); 21 | 22 | frame.remove(); 23 | }); 24 | 25 | $(document.body).insert(frame); 26 | } 27 | 28 | var features = {}; 29 | 30 | function detectParagraphType(document) { 31 | document.body.innerHTML = ''; 32 | document.execCommand('insertparagraph', false, null); 33 | 34 | var tagName; 35 | element = document.body.childNodes[0]; 36 | if (element && element.tagName) 37 | tagName = element.tagName.toLowerCase(); 38 | 39 | if (tagName == 'div') 40 | features.paragraphType = "div"; 41 | else if (document.body.innerHTML == "


") 42 | features.paragraphType = "br"; 43 | else 44 | features.paragraphType = "p"; 45 | } 46 | 47 | function detectIndentType(document) { 48 | document.body.innerHTML = 'tab'; 49 | document.execCommand('indent', false, null); 50 | 51 | var tagName; 52 | element = document.body.childNodes[0]; 53 | if (element && element.tagName) 54 | tagName = element.tagName.toLowerCase(); 55 | features.indentInsertsBlockquote = (tagName == 'blockquote'); 56 | } 57 | 58 | features.run = function run() { 59 | if (features.finished) return; 60 | 61 | createTmpIframe(function(document) { 62 | detectParagraphType(document); 63 | detectIndentType(document); 64 | 65 | features.finished = true; 66 | }); 67 | } 68 | 69 | return features; 70 | })(); 71 | -------------------------------------------------------------------------------- /src/wysihat/formatting.js: -------------------------------------------------------------------------------- 1 | WysiHat.Formatting = (function() { 2 | var ACCUMULATING_LINE = {}; 3 | var EXPECTING_LIST_ITEM = {}; 4 | var ACCUMULATING_LIST_ITEM = {}; 5 | 6 | return { 7 | getBrowserMarkupFrom: function(applicationMarkup) { 8 | var container = new Element("div").update(applicationMarkup); 9 | 10 | function spanify(element, style) { 11 | element.replace( 12 | '' + 14 | element.innerHTML + '' 15 | ); 16 | } 17 | 18 | function convertStrongsToSpans() { 19 | container.select("strong").each(function(element) { 20 | spanify(element, "font-weight: bold"); 21 | }); 22 | } 23 | 24 | function convertEmsToSpans() { 25 | container.select("em").each(function(element) { 26 | spanify(element, "font-style: italic"); 27 | }); 28 | } 29 | 30 | function convertDivsToParagraphs() { 31 | container.select("div").each(function(element) { 32 | element.replace("

" + element.innerHTML + "

"); 33 | }); 34 | } 35 | 36 | if (Prototype.Browser.WebKit || Prototype.Browser.Gecko) { 37 | convertStrongsToSpans(); 38 | convertEmsToSpans(); 39 | } else if (Prototype.Browser.IE || Prototype.Browser.Opera) { 40 | convertDivsToParagraphs(); 41 | } 42 | 43 | return container.innerHTML; 44 | }, 45 | 46 | getApplicationMarkupFrom: function(element) { 47 | var mode = ACCUMULATING_LINE, result, container, line, lineContainer, previousAccumulation; 48 | 49 | function walk(nodes) { 50 | var length = nodes.length, node, tagName, i; 51 | 52 | for (i = 0; i < length; i++) { 53 | node = nodes[i]; 54 | 55 | if (node.nodeType == Node.ELEMENT_NODE) { 56 | tagName = node.tagName.toLowerCase(); 57 | open(tagName, node); 58 | walk(node.childNodes); 59 | close(tagName); 60 | 61 | } else if (node.nodeType == Node.TEXT_NODE) { 62 | read(node.nodeValue); 63 | } 64 | } 65 | } 66 | 67 | function open(tagName, node) { 68 | if (mode == ACCUMULATING_LINE) { 69 | // if it's a block-level element and the line buffer is full, flush it 70 | if (isBlockElement(tagName)) { 71 | if (isEmptyParagraph(node)) { 72 | accumulate(new Element("br")); 73 | } 74 | 75 | flush(); 76 | 77 | // if it's a ul or ol, switch to expecting-list-item mode 78 | if (isListElement(tagName)) { 79 | container = insertList(tagName); 80 | mode = EXPECTING_LIST_ITEM; 81 | } 82 | 83 | } else if (isLineBreak(tagName)) { 84 | // if it's a br, and the previous accumulation was a br, 85 | // remove the previous accumulation and flush 86 | if (isLineBreak(getPreviouslyAccumulatedTagName())) { 87 | previousAccumulation.parentNode.removeChild(previousAccumulation); 88 | flush(); 89 | } 90 | 91 | // accumulate the br 92 | accumulate(node.cloneNode(false)); 93 | 94 | // if it's the first br in a line, flush 95 | if (!previousAccumulation.previousNode) flush(); 96 | 97 | } else { 98 | accumulateInlineElement(tagName, node); 99 | } 100 | 101 | } else if (mode == EXPECTING_LIST_ITEM) { 102 | if (isListItemElement(tagName)) { 103 | mode = ACCUMULATING_LIST_ITEM; 104 | } 105 | 106 | } else if (mode == ACCUMULATING_LIST_ITEM) { 107 | if (isLineBreak(tagName)) { 108 | accumulate(node.cloneNode(false)); 109 | 110 | } else if (!isBlockElement(tagName)) { 111 | accumulateInlineElement(tagName, node); 112 | } 113 | } 114 | } 115 | 116 | function close(tagName) { 117 | if (mode == ACCUMULATING_LINE) { 118 | if (isLineElement(tagName)) { 119 | flush(); 120 | } 121 | 122 | if (line != lineContainer) { 123 | lineContainer = lineContainer.parentNode; 124 | } 125 | 126 | } else if (mode == EXPECTING_LIST_ITEM) { 127 | if (isListElement(tagName)) { 128 | container = result; 129 | mode = ACCUMULATING_LINE; 130 | } 131 | 132 | } else if (mode == ACCUMULATING_LIST_ITEM) { 133 | if (isListItemElement(tagName)) { 134 | flush(); 135 | mode = EXPECTING_LIST_ITEM; 136 | } 137 | 138 | if (line != lineContainer) { 139 | lineContainer = lineContainer.parentNode; 140 | } 141 | } 142 | } 143 | 144 | function isBlockElement(tagName) { 145 | return isLineElement(tagName) || isListElement(tagName); 146 | } 147 | 148 | function isLineElement(tagName) { 149 | return tagName == "p" || tagName == "div"; 150 | } 151 | 152 | function isListElement(tagName) { 153 | return tagName == "ol" || tagName == "ul"; 154 | } 155 | 156 | function isListItemElement(tagName) { 157 | return tagName == "li"; 158 | } 159 | 160 | function isLineBreak(tagName) { 161 | return tagName == "br"; 162 | } 163 | 164 | function isEmptyParagraph(node) { 165 | return node.tagName.toLowerCase() == "p" && node.childNodes.length == 0; 166 | } 167 | 168 | function read(value) { 169 | accumulate(document.createTextNode(value)); 170 | } 171 | 172 | function accumulateInlineElement(tagName, node) { 173 | var element = node.cloneNode(false); 174 | 175 | if (tagName == "span") { 176 | if ($(node).getStyle("fontWeight") == "bold") { 177 | element = new Element("strong"); 178 | 179 | } else if ($(node).getStyle("fontStyle") == "italic") { 180 | element = new Element("em"); 181 | } 182 | } 183 | 184 | accumulate(element); 185 | lineContainer = element; 186 | } 187 | 188 | function accumulate(node) { 189 | if (mode != EXPECTING_LIST_ITEM) { 190 | if (!line) line = lineContainer = createLine(); 191 | previousAccumulation = node; 192 | lineContainer.appendChild(node); 193 | } 194 | } 195 | 196 | function getPreviouslyAccumulatedTagName() { 197 | if (previousAccumulation && previousAccumulation.nodeType == Node.ELEMENT_NODE) { 198 | return previousAccumulation.tagName.toLowerCase(); 199 | } 200 | } 201 | 202 | function flush() { 203 | if (line && line.childNodes.length) { 204 | container.appendChild(line); 205 | line = lineContainer = null; 206 | } 207 | } 208 | 209 | function createLine() { 210 | if (mode == ACCUMULATING_LINE) { 211 | return new Element("div"); 212 | } else if (mode == ACCUMULATING_LIST_ITEM) { 213 | return new Element("li"); 214 | } 215 | } 216 | 217 | function insertList(tagName) { 218 | var list = new Element(tagName); 219 | result.appendChild(list); 220 | return list; 221 | } 222 | 223 | result = container = new Element("div"); 224 | walk(element.childNodes); 225 | flush(); 226 | return result.innerHTML; 227 | } 228 | }; 229 | })(); 230 | -------------------------------------------------------------------------------- /src/wysihat/header.js: -------------------------------------------------------------------------------- 1 | /* WysiHat - WYSIWYG JavaScript framework, version 0.2.1 2 | * (c) 2008-2010 Joshua Peek 3 | * 4 | * WysiHat is freely distributable under the terms of an MIT-style license. 5 | *--------------------------------------------------------------------------*/ 6 | 7 | /** 8 | * == wysihat == 9 | **/ 10 | 11 | /** section: wysihat 12 | * WysiHat 13 | **/ 14 | var WysiHat = {}; 15 | -------------------------------------------------------------------------------- /src/wysihat/toolbar.js: -------------------------------------------------------------------------------- 1 | //= require "./events/selection_change" 2 | 3 | /** section: wysihat 4 | * class WysiHat.Toolbar 5 | **/ 6 | WysiHat.Toolbar = Class.create((function() { 7 | /** 8 | * new WysiHat.Toolbar(editor) 9 | * - editor (WysiHat.Editor): the editor object that you want to attach to 10 | * 11 | * Creates a toolbar element above the editor. The WysiHat.Toolbar object 12 | * has many helper methods to easily add buttons to the toolbar. 13 | * 14 | * This toolbar class is not required for the Editor object to function. 15 | * It is merely a set of helper methods to get you started and to build 16 | * on top of. If you are going to use this class in your application, 17 | * it is highly recommended that you subclass it and override methods 18 | * to add custom functionality. 19 | **/ 20 | function initialize(editor) { 21 | this.editor = editor; 22 | this.element = this.createToolbarElement(); 23 | } 24 | 25 | /** 26 | * WysiHat.Toolbar#createToolbarElement() -> Element 27 | * 28 | * Creates a toolbar container element and inserts it right above the 29 | * original textarea element. The element is a div with the class 30 | * 'editor_toolbar'. 31 | * 32 | * You can override this method to customize the element attributes and 33 | * insert position. Be sure to return the element after it has been 34 | * inserted. 35 | **/ 36 | function createToolbarElement() { 37 | var toolbar = new Element('div', { 'class': 'editor_toolbar' }); 38 | this.editor.insert({before: toolbar}); 39 | return toolbar; 40 | } 41 | 42 | /** 43 | * WysiHat.Toolbar#addButtonSet(set) -> undefined 44 | * - set (Array): The set array contains nested arrays that hold the 45 | * button options, and handler. 46 | * 47 | * Adds a button set to the toolbar. 48 | **/ 49 | function addButtonSet(set) { 50 | $A(set).each(function(button){ 51 | this.addButton(button); 52 | }.bind(this)); 53 | } 54 | 55 | /** 56 | * WysiHat.Toolbar#addButton(options[, handler]) -> undefined 57 | * - options (Hash): Required options hash 58 | * - handler (Function): Function to bind to the button 59 | * 60 | * The options hash accepts two required keys, name and label. The label 61 | * value is used as the link's inner text. The name value is set to the 62 | * link's class and is used to check the button state. However the name 63 | * may be omitted if the name and label are the same. In that case, the 64 | * label will be down cased to make the name value. So a "Bold" label 65 | * will default to "bold" name. 66 | * 67 | * The second optional handler argument will be used if no handler 68 | * function is supplied in the options hash. 69 | * 70 | * toolbar.addButton({ 71 | * name: 'bold', label: "Bold" }, function(editor) { 72 | * editor.boldSelection(); 73 | * }); 74 | * 75 | * Would create a link, 76 | * "Bold" 77 | **/ 78 | function addButton(options, handler) { 79 | options = $H(options); 80 | 81 | if (!options.get('name')) 82 | options.set('name', options.get('label').toLowerCase()); 83 | var name = options.get('name'); 84 | 85 | var button = this.createButtonElement(this.element, options); 86 | 87 | var handler = this.buttonHandler(name, options); 88 | this.observeButtonClick(button, handler); 89 | 90 | var handler = this.buttonStateHandler(name, options); 91 | this.observeStateChanges(button, name, handler); 92 | } 93 | 94 | /** 95 | * WysiHat.Toolbar#createButtonElement(toolbar, options) -> Element 96 | * - toolbar (Element): Toolbar element created by createToolbarElement 97 | * - options (Hash): Options hash that pass from addButton 98 | * 99 | * Creates individual button elements and inserts them into the toolbar 100 | * container. The default elements are 'a' tags with a 'button' class. 101 | * 102 | * You can override this method to customize the element attributes and 103 | * insert positions. Be sure to return the element after it has been 104 | * inserted. 105 | **/ 106 | function createButtonElement(toolbar, options) { 107 | var button = new Element('a', { 108 | 'class': 'button', 'href': '#' 109 | }); 110 | button.update('' + options.get('label') + ''); 111 | button.addClassName(options.get('name')); 112 | 113 | toolbar.appendChild(button); 114 | 115 | return button; 116 | } 117 | 118 | /** 119 | * WysiHat.Toolbar#buttonHandler(name, options) -> Function 120 | * - name (String): Name of button command: 'bold', 'italic' 121 | * - options (Hash): Options hash that pass from addButton 122 | * 123 | * Returns the button handler function to bind to the buttons onclick 124 | * event. It checks the options for a 'handler' attribute otherwise it 125 | * defaults to a function that calls execCommand with the button name. 126 | **/ 127 | function buttonHandler(name, options) { 128 | if (options.handler) 129 | return options.handler; 130 | else if (options.get('handler')) 131 | return options.get('handler'); 132 | else 133 | return function(editor) { editor.execCommand(name); }; 134 | } 135 | 136 | /** 137 | * WysiHat.Toolbar#observeButtonClick(element, handler) -> undefined 138 | * - element (Element): Button element 139 | * - handler (Function): Handler function to bind to element 140 | * 141 | * Bind handler to elements onclick event. 142 | **/ 143 | function observeButtonClick(element, handler) { 144 | element.on('click', function(event) { 145 | handler(this.editor); 146 | event.stop(); 147 | }.bind(this)); 148 | } 149 | 150 | /** 151 | * WysiHat.Toolbar#buttonStateHandler(name, options) -> Function 152 | * - name (String): Name of button command: 'bold', 'italic' 153 | * - options (Hash): Options hash that pass from addButton 154 | * 155 | * Returns the button handler function that checks whether the button 156 | * state is on (true) or off (false). It checks the options for a 157 | * 'query' attribute otherwise it defaults to a function that calls 158 | * queryCommandState with the button name. 159 | **/ 160 | function buttonStateHandler(name, options) { 161 | if (options.query) 162 | return options.query; 163 | else if (options.get('query')) 164 | return options.get('query'); 165 | else 166 | return function(editor) { return editor.queryCommandState(name); }; 167 | } 168 | 169 | /** 170 | * WysiHat.Toolbar#observeStateChanges(element, name, handler) -> undefined 171 | * - element (Element): Button element 172 | * - name (String): Button name 173 | * - handler (Function): State query function 174 | * 175 | * Determines buttons state by calling the query handler function then 176 | * calls updateButtonState. 177 | **/ 178 | function observeStateChanges(element, name, handler) { 179 | var previousState; 180 | this.editor.on("selection:change", function(event) { 181 | var state = handler(this.editor); 182 | if (state != previousState) { 183 | previousState = state; 184 | this.updateButtonState(element, name, state); 185 | } 186 | }.bind(this)); 187 | } 188 | 189 | /** 190 | * WysiHat.Toolbar#updateButtonState(element, name, state) -> undefined 191 | * - element (Element): Button element 192 | * - name (String): Button name 193 | * - state (Boolean): Whether button state is on/off 194 | * 195 | * If the state is on, it adds a 'selected' class to the button element. 196 | * Otherwise it removes the 'selected' class. 197 | * 198 | * You can override this method to change the class name or styles 199 | * applied to buttons when their state changes. 200 | **/ 201 | function updateButtonState(element, name, state) { 202 | if (state) 203 | element.addClassName('selected'); 204 | else 205 | element.removeClassName('selected'); 206 | } 207 | 208 | return { 209 | initialize: initialize, 210 | createToolbarElement: createToolbarElement, 211 | addButtonSet: addButtonSet, 212 | addButton: addButton, 213 | createButtonElement: createButtonElement, 214 | buttonHandler: buttonHandler, 215 | observeButtonClick: observeButtonClick, 216 | buttonStateHandler: buttonStateHandler, 217 | observeStateChanges: observeStateChanges, 218 | updateButtonState: updateButtonState 219 | }; 220 | })()); 221 | 222 | /** 223 | * WysiHat.Toolbar.ButtonSets 224 | * 225 | * A namespace for various sets of Toolbar buttons. These sets should be 226 | * compatible with WysiHat.Toolbar, and can be added to the toolbar with: 227 | * toolbar.addButtonSet(WysiHat.Toolbar.ButtonSets.Basic); 228 | **/ 229 | WysiHat.Toolbar.ButtonSets = {}; 230 | 231 | /** 232 | * WysiHat.Toolbar.ButtonSets.Basic 233 | * 234 | * A basic set of buttons: bold, underline, and italic. This set is 235 | * compatible with WysiHat.Toolbar, and can be added to the toolbar with: 236 | * toolbar.addButtonSet(WysiHat.Toolbar.ButtonSets.Basic); 237 | **/ 238 | WysiHat.Toolbar.ButtonSets.Basic = $A([ 239 | { label: "Bold" }, 240 | { label: "Underline" }, 241 | { label: "Italic" } 242 | ]); 243 | -------------------------------------------------------------------------------- /test/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WYSIWYG 5 | 6 | 47 | 48 | 49 | 50 | 51 | 93 | 94 | 95 | 96 |
97 |
98 | 99 |

Formatted Contents

100 | 101 | 102 |

Raw Contents

103 | 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /test/unit/editor_test.js: -------------------------------------------------------------------------------- 1 | new Test.Unit.Runner({ 2 | setup: function() { 3 | this.textarea = $('content'); 4 | this.editor = WysiHat.Editor.attach(this.textarea); 5 | this.editor.focus(); 6 | }, 7 | 8 | teardown: function() { 9 | this.editor.innerHTML = ""; 10 | this.textarea.value = ""; 11 | }, 12 | 13 | testInsertHTML: function() { 14 | var runner = this; 15 | 16 | this.editor.insertHTML("

Hello.

"); 17 | runner.assertEqual("

Hello.

", this.editor.innerHTML); 18 | }, 19 | 20 | testBoldSelection: function() { 21 | var runner = this; 22 | 23 | // this.editor.insertHTML("

Hello.

"); 24 | this.editor.innerHTML = '

Hello.

'.formatHTMLInput(); 25 | 26 | window.getSelection().selectNode(this.editor.down('#hello')); 27 | this.editor.boldSelection(); 28 | 29 | runner.assert(this.editor.boldSelected()); 30 | runner.assertEqual('

Hello.

', this.editor.innerHTML); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/features_test.js: -------------------------------------------------------------------------------- 1 | new Test.Unit.Runner({ 2 | setup: function() { 3 | WysiHat.BrowserFeatures.run(); 4 | }, 5 | 6 | testDetectParagraphType: function() { 7 | var runner = this; 8 | 9 | runner.wait(1000, function() { 10 | if (Prototype.Browser.WebKit) 11 | runner.assertEqual("div", WysiHat.BrowserFeatures.paragraphType); 12 | else if (Prototype.Browser.Gecko) 13 | runner.assertEqual("br", WysiHat.BrowserFeatures.paragraphType); 14 | else if (Prototype.Browser.IE) 15 | runner.assertEqual("p", WysiHat.BrowserFeatures.paragraphType); 16 | }); 17 | }, 18 | 19 | testDetectIndentType: function() { 20 | var runner = this; 21 | 22 | runner.wait(1000, function() { 23 | if (Prototype.Browser.WebKit || Prototype.Browser.IE) 24 | runner.assertEqual(true, WysiHat.BrowserFeatures.indentInsertsBlockquote); 25 | else if (Prototype.Browser.Gecko) 26 | runner.assertEqual(false, WysiHat.BrowserFeatures.indentInsertsBlockquote); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/fixtures/editor.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /test/unit/fixtures/range.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /test/unit/fixtures/selection.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /test/unit/formatting_test.js: -------------------------------------------------------------------------------- 1 | function getBrowserMarkupFrom(html) { 2 | return WysiHat.Formatting.getBrowserMarkupFrom(html); 3 | } 4 | 5 | function getApplicationMarkupFrom(html) { 6 | var element = new Element("div").update(html); 7 | return WysiHat.Formatting.getApplicationMarkupFrom(element); 8 | } 9 | 10 | new Test.Unit.Runner({ 11 | testGetBrowserMarkupFrom: function() { 12 | var runner = this; 13 | 14 | if (Prototype.Browser.WebKit) { 15 | runner.assertEqual( 16 | '
Here is some basic text
with a line break.

And maybe another paragraph
', 17 | getBrowserMarkupFrom('
Here is some basic text
with a line break.

And maybe another paragraph
') 18 | ); 19 | runner.assertEqual( 20 | '
Some bold text
"', 21 | getBrowserMarkupFrom('
Some bold text
"') 22 | ); 23 | } else if (Prototype.Browser.Gecko) { 24 | runner.assertEqual( 25 | '
Here is some basic text
with a line break.

And maybe another paragraph
', 26 | getBrowserMarkupFrom('
Here is some basic text
with a line break.

And maybe another paragraph
') 27 | ); 28 | runner.assertEqual( 29 | '
Some bold text
"', 30 | getBrowserMarkupFrom('
Some bold text
"') 31 | ); 32 | } else if (Prototype.Browser.IE) { 33 | runner.assertEqual( 34 | '

Here is some basic text

\r\n

with a line break.

\r\n


\r\n

And maybe another paragraph

', 35 | getBrowserMarkupFrom('
Here is some basic text
with a line break.

And maybe another paragraph
') 36 | ); 37 | runner.assertEqual( 38 | '

Some bold text

"', 39 | getBrowserMarkupFrom('
Some bold text
"') 40 | ); 41 | } 42 | }, 43 | 44 | testGetApplicationMarkupFrom: function() { 45 | var runner = this; 46 | 47 | if (Prototype.Browser.WebKit) { 48 | runner.assertEqual( 49 | '
Here is some basic text
with a line break.

And maybe another paragraph
', 50 | getApplicationMarkupFrom('
Here is some basic text
with a line break.

And maybe another paragraph
') 51 | ); 52 | runner.assertEqual( 53 | '
Hello

World!
', 54 | getApplicationMarkupFrom('
Hello

World!
') 55 | ); 56 | runner.assertEqual( 57 | '
Hello

World!
', 58 | getApplicationMarkupFrom('

Hello


World!
') 59 | ); 60 | runner.assertEqual( 61 | '
Hello


World!
', 62 | getApplicationMarkupFrom('
Hello

World!

') 63 | ); 64 | runner.assertEqual( 65 | '
Hello
World!
Goodbye!
', 66 | getApplicationMarkupFrom('Hello
World!
Goodbye!
') 67 | ); 68 | runner.assertEqual( 69 | '
Hello
World!
Goodbye!
', 70 | getApplicationMarkupFrom('Hello
World!
Goodbye!
') 71 | ); 72 | runner.assertEqual( 73 | '
Hello

World!
Goodbye!
', 74 | getApplicationMarkupFrom('Hello

World!
Goodbye!
') 75 | ); 76 | runner.assertEqual( 77 | '
Hello
World!

Goodbye!
', 78 | getApplicationMarkupFrom('Hello
World!

Goodbye!
') 79 | ); 80 | runner.assertEqual( 81 | '
Some bold text
', 82 | getApplicationMarkupFrom('Some bold text') 83 | ); 84 | runner.assertEqual( 85 | '
Some italic text
', 86 | getApplicationMarkupFrom('Some italic text') 87 | ); 88 | runner.assertEqual( 89 | '
Some underlined text
', 90 | getApplicationMarkupFrom('Some underlined text') 91 | ); 92 | runner.assertEqual( 93 | '
Some bold and italic text
', 94 | getApplicationMarkupFrom('Some bold and italic text') 95 | ); 96 | runner.assertEqual( 97 | '
Some italic and bold text
', 98 | getApplicationMarkupFrom('Some italic and bold text') 99 | ); 100 | runner.assertEqual( 101 | '
Some bold, underlined, and italic text
', 102 | getApplicationMarkupFrom('Some bold, underlined, and italic text') 103 | ); 104 | runner.assertEqual( 105 | '
Hello
', 106 | getApplicationMarkupFrom('
Hello
') 107 | ); 108 | runner.assertEqual( 109 | '
Hello
', 110 | getApplicationMarkupFrom('
Hello
') 111 | ); 112 | runner.assertEqual( 113 | '
', 114 | getApplicationMarkupFrom('') 115 | ); 116 | runner.assertEqual( 117 | '
  1. one
  2. two
not
', 118 | getApplicationMarkupFrom('
  1. one
  2. two
not
') 119 | ); 120 | runner.assertEqual( 121 | '
not
', 122 | getApplicationMarkupFrom('
not
') 123 | ); 124 | } else if (Prototype.Browser.Gecko) { 125 | runner.assertEqual( 126 | '
Here is some basic text
with a line break.

And maybe another paragraph
', 127 | getApplicationMarkupFrom('Here is some basic text
with a line break.

And maybe another paragraph
') 128 | ); 129 | runner.assertEqual( 130 | '
Here is some basic text
with a line break.

And maybe another paragraph
', 131 | getApplicationMarkupFrom('Here is some basic text
with a line break.


And maybe another paragraph
') 132 | ); 133 | runner.assertEqual( 134 | '
Hello

World!
', 135 | getApplicationMarkupFrom('Hello

World!') 136 | ); 137 | runner.assertEqual( 138 | '
Hello
World!
Goodbye!
', 139 | getApplicationMarkupFrom('Hello
World!
Goodbye!
') 140 | ); 141 | runner.assertEqual( 142 | '
Hello
World!

Goodbye!
', 143 | getApplicationMarkupFrom('Hello
World!

Goodbye!
') 144 | ); 145 | runner.assertEqual( 146 | '
Some bold text
', 147 | getApplicationMarkupFrom('Some bold text') 148 | ); 149 | runner.assertEqual( 150 | '
Some italic text
', 151 | getApplicationMarkupFrom('Some italic text') 152 | ); 153 | runner.assertEqual( 154 | '
Some underlined text
', 155 | getApplicationMarkupFrom('Some underlined text') 156 | ); 157 | runner.assertEqual( 158 | '
Some bold and italic text
', // BROKEN 159 | getApplicationMarkupFrom('Some bold and italic text') 160 | ); 161 | runner.assertEqual( 162 | '
Some italic and bold text
', // BROKEN 163 | getApplicationMarkupFrom('Some italic and bold text') 164 | ); 165 | runner.assertEqual( 166 | '
Some bold, underline, and italic text
', // BROKEN 167 | getApplicationMarkupFrom('Some bold, underline, and italic text') 168 | ); 169 | runner.assertEqual( 170 | '
', 171 | getApplicationMarkupFrom('') 172 | ); 173 | runner.assertEqual( 174 | '
  1. one
  2. two
not
', 175 | getApplicationMarkupFrom('
  1. one
  2. two
not
') 176 | ); 177 | runner.assertEqual( 178 | '
not
', 179 | getApplicationMarkupFrom('not
') 180 | ); 181 | } else if (Prototype.Browser.IE) { 182 | runner.assertEqual( 183 | '

', 184 | getApplicationMarkupFrom('

') 185 | ); 186 | runner.assertEqual( 187 | '

\r\n

', 188 | getApplicationMarkupFrom('

\n

') 189 | ); 190 | runner.assertEqual( 191 | '

\r\n
 
\r\n

', 192 | getApplicationMarkupFrom('

\n

 

\n

') 193 | ); 194 | runner.assertEqual( 195 | '
 
\r\n

', 196 | getApplicationMarkupFrom('

 

\n

') 197 | ); 198 | runner.assertEqual( 199 | '

\r\n
One
\r\n

\r\n
Two
\r\n

\r\n

\r\n
Break
\r\n

', 200 | getApplicationMarkupFrom('

\n

One


\n

Two

\n

\n

\n

Break

\n

') 201 | ); 202 | runner.assertEqual( 203 | '

\r\n
Here is some basic text
\r\n

\r\n
with a line break.
\r\n

\r\n
 
\r\n

\r\n
And maybe another paragraph
\r\n

', 204 | getApplicationMarkupFrom('

\n

Here is some basic text


\n

with a line break.


\n

 


\n

And maybe another paragraph

\n

') 205 | ); 206 | runner.assertEqual( 207 | '

\r\n
Hello
\r\n

\r\n
 
\r\n

\r\n
World!
\r\n

', 208 | getApplicationMarkupFrom('

\n

Hello


\n

 


\n

World!

\n

') 209 | ); 210 | runner.assertEqual( 211 | '

\r\n
Hello
\r\n

\r\n
World!
\r\n

\r\n
Goodbye!
\r\n

', 212 | getApplicationMarkupFrom('

\n

Hello


\n

World!


\n

Goodbye!

\n

') 213 | ); 214 | runner.assertEqual( 215 | '

\r\n
Hello
\r\n

\r\n
World!
\r\n

\r\n
 
\r\n

\r\n
Goodbye!
\r\n

', 216 | getApplicationMarkupFrom('

\n

Hello


\n

World!


\n

 


\n

Goodbye!

\n

') 217 | ); 218 | runner.assertEqual( 219 | '
Some bold text
', 220 | getApplicationMarkupFrom('Some bold text') 221 | ); 222 | runner.assertEqual( 223 | '
Some italic text
', 224 | getApplicationMarkupFrom('Some italic text') 225 | ); 226 | runner.assertEqual( 227 | '
Some underlined text
', 228 | getApplicationMarkupFrom('Some underlined text') 229 | ); 230 | runner.assertEqual( 231 | '
    \r\n
  1. one
  2. \r\n
  3. two
\r\n
not
', 232 | getApplicationMarkupFrom('
    \n
  1. one
  2. \n
  3. two
\n

not

') 233 | ); 234 | runner.assertEqual( 235 | '\r\n
not
', 236 | getApplicationMarkupFrom('\n

not

') 237 | ); 238 | } 239 | } 240 | }); 241 | -------------------------------------------------------------------------------- /test/unit/frame_loaded_test.js: -------------------------------------------------------------------------------- 1 | new Test.Unit.Runner({ 2 | testFrameLoaded: function() { 3 | var runner = this; 4 | 5 | frame = new Element('iframe'); 6 | 7 | var frameLoaded = false; 8 | frame.onFrameLoaded(function() { frameLoaded = true; }); 9 | 10 | $(document.body).insert(frame); 11 | 12 | runner.wait(1000, function() { 13 | runner.assert(frameLoaded); 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/range_test.js: -------------------------------------------------------------------------------- 1 | new Test.Unit.Runner({ 2 | setup: function() { 3 | $('content').update( 4 | "Lorem ipsum dolor sit amet, " + 5 | "consectetuer adipiscing elit." 6 | ); 7 | 8 | $('wrapper').cleanWhitespace(); 9 | 10 | this.range = document.createRange(); 11 | this.range.selectNode($('content')); 12 | }, 13 | 14 | testSetStart: function() { 15 | var runner = this; 16 | 17 | this.range.setStart($('content'), 2); 18 | this.range.setEnd($('content'), 2); 19 | 20 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 21 | runner.assertEqual(2, this.range.startOffset, "startOffset"); 22 | runner.assertEqual($('content'), this.range.endContainer, "endContainer"); 23 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 24 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 25 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 26 | runner.assertEqual("", this.range.toString(), "toString"); 27 | 28 | this.range.setStart($('lorem'), 0); 29 | this.range.setEnd($('lorem'), 1); 30 | 31 | runner.assertEqual($('lorem'), this.range.startContainer, "startContainer"); 32 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 33 | runner.assertEqual($('lorem'), this.range.endContainer, "endContainer"); 34 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 35 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 36 | runner.assertEqual($('lorem'), this.range.commonAncestorContainer, "commonAncestorContainer"); 37 | runner.assertEqual("Lorem ipsum", this.range.toString(), "toString"); 38 | }, 39 | 40 | testSetEnd: function() { 41 | var runner = this; 42 | 43 | this.range.setStart($('content'), 1); 44 | this.range.setEnd($('content'), 2); 45 | 46 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 47 | runner.assertEqual(1, this.range.startOffset, "startOffset"); 48 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 49 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 50 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 51 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 52 | runner.assertEqual(" dolor sit amet, ", this.range.toString(), "toString"); 53 | 54 | this.range.setStart($('consectetuer'), 0); 55 | this.range.setEnd($('consectetuer'), 1); 56 | 57 | runner.assertEqual($('consectetuer'), this.range.startContainer, "startContainer"); 58 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 59 | runner.assertEqual($('consectetuer'), this.range.endContainer, "startContainer"); 60 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 61 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 62 | runner.assertEqual($('consectetuer'), this.range.commonAncestorContainer, "commonAncestorContainer"); 63 | runner.assertEqual("consectetuer", this.range.toString(), "toString"); 64 | }, 65 | 66 | testSetStartBefore: function() { 67 | var runner = this; 68 | 69 | this.range.setStartBefore($('lorem')); 70 | this.range.setEnd($('content'), 2); 71 | 72 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 73 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 74 | runner.assertEqual($('content'), this.range.endContainer, "endContainer"); 75 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 76 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 77 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 78 | runner.assertEqual("Lorem ipsum dolor sit amet, ", this.range.toString(), "toString"); 79 | 80 | this.range.setStartBefore($('content')); 81 | this.range.setEnd($('content'), 2); 82 | 83 | runner.assertEqual($('wrapper'), this.range.startContainer, "startContainer"); 84 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 85 | runner.assertEqual($('content'), this.range.endContainer, "endContainer"); 86 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 87 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 88 | runner.assertEqual($('wrapper'), this.range.commonAncestorContainer, "commonAncestorContainer"); 89 | runner.assertEqual("Lorem ipsum dolor sit amet, ", this.range.toString(), "toString"); 90 | }, 91 | 92 | testSetStartAfter: function() { 93 | var runner = this; 94 | 95 | this.range.setStartAfter($('lorem')); 96 | this.range.setEnd($('content'), 2); 97 | 98 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 99 | runner.assertEqual(1, this.range.startOffset, "startOffset"); 100 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 101 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 102 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 103 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 104 | runner.assertEqual(" dolor sit amet, ", this.range.toString(), "toString"); 105 | 106 | this.range.setStartAfter($('content')); 107 | this.range.setEnd($('wrapper'), 1); 108 | 109 | runner.assertEqual($('wrapper'), this.range.startContainer, "startContainer"); 110 | runner.assertEqual(1, this.range.startOffset, "startOffset"); 111 | runner.assertEqual($('wrapper'), this.range.endContainer, "startContainer"); 112 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 113 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 114 | runner.assertEqual($('wrapper'), this.range.commonAncestorContainer, "commonAncestorContainer"); 115 | runner.assertEqual("", this.range.toString(), "toString"); 116 | }, 117 | 118 | testSetEndBefore: function() { 119 | var runner = this; 120 | 121 | this.range.setStart($('content'), 0); 122 | this.range.setEndBefore($('lorem')); 123 | 124 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 125 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 126 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 127 | runner.assertEqual(0, this.range.endOffset, "endOffset"); 128 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 129 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 130 | runner.assertEqual("", this.range.toString(), "toString"); 131 | 132 | this.range.setStart($('wrapper'), 0); 133 | this.range.setEndBefore($('content')); 134 | 135 | runner.assertEqual($('wrapper'), this.range.startContainer, "startContainer"); 136 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 137 | runner.assertEqual($('wrapper'), this.range.endContainer, "startContainer"); 138 | runner.assertEqual(0, this.range.endOffset, "endOffset"); 139 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 140 | runner.assertEqual($('wrapper'), this.range.commonAncestorContainer, "commonAncestorContainer"); 141 | runner.assertEqual("", this.range.toString(), "toString"); 142 | }, 143 | 144 | testSetEndAfter: function() { 145 | var runner = this; 146 | 147 | this.range.setStart($('content'), 0); 148 | this.range.setEndAfter($('lorem')); 149 | 150 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 151 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 152 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 153 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 154 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 155 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 156 | runner.assertEqual("Lorem ipsum", this.range.toString(), "toString"); 157 | 158 | this.range.setStart($('content'), 0); 159 | this.range.setEndAfter($('content')); 160 | 161 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 162 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 163 | runner.assertEqual($('wrapper'), this.range.endContainer, "startContainer"); 164 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 165 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 166 | runner.assertEqual($('wrapper'), this.range.commonAncestorContainer, "commonAncestorContainer"); 167 | runner.assertEqual("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", this.range.toString(), "toString"); 168 | }, 169 | 170 | testCollapse: function() { 171 | var runner = this; 172 | 173 | this.range.setStart($('content'), 1); 174 | this.range.setEnd($('content'), 2); 175 | this.range.collapse(true); 176 | 177 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 178 | runner.assertEqual(1, this.range.startOffset, "startOffset"); 179 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 180 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 181 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 182 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 183 | runner.assertEqual("", this.range.toString(), "toString"); 184 | 185 | this.range.setStart($('content'), 1); 186 | this.range.setEnd($('content'), 2); 187 | this.range.collapse(false); 188 | 189 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 190 | runner.assertEqual(2, this.range.startOffset, "startOffset"); 191 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 192 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 193 | runner.assertEqual(true, this.range.collapsed, "collapsed"); 194 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 195 | runner.assertEqual("", this.range.toString(), "toString"); 196 | }, 197 | 198 | testSelectNode: function() { 199 | var runner = this; 200 | 201 | this.range.selectNode($('content')); 202 | 203 | runner.assertEqual($('wrapper'), this.range.startContainer, "startContainer"); 204 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 205 | runner.assertEqual($('wrapper'), this.range.endContainer, "startContainer"); 206 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 207 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 208 | runner.assertEqual($('wrapper'), this.range.commonAncestorContainer, "commonAncestorContainer"); 209 | runner.assertEqual("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", this.range.toString(), "toString"); 210 | 211 | this.range.selectNode($('lorem')); 212 | 213 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 214 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 215 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 216 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 217 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 218 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 219 | runner.assertEqual("Lorem ipsum", this.range.toString(), "toString"); 220 | }, 221 | 222 | testSelectNodeContents: function() { 223 | var runner = this; 224 | 225 | this.range.selectNodeContents($('content')); 226 | 227 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 228 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 229 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 230 | runner.assertEqual(4, this.range.endOffset, "endOffset"); 231 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 232 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 233 | runner.assertEqual("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", this.range.toString(), "toString"); 234 | 235 | this.range.selectNodeContents($('lorem')); 236 | 237 | runner.assertEqual($('lorem'), this.range.startContainer, "startContainer"); 238 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 239 | runner.assertEqual($('lorem'), this.range.endContainer, "startContainer"); 240 | runner.assertEqual(1, this.range.endOffset, "endOffset"); 241 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 242 | runner.assertEqual($('lorem'), this.range.commonAncestorContainer, "commonAncestorContainer"); 243 | runner.assertEqual("Lorem ipsum", this.range.toString(), "toString"); 244 | }, 245 | 246 | testDeleteContents: function() { 247 | var runner = this; 248 | 249 | this.range.selectNodeContents($('lorem')); 250 | this.range.deleteContents(); 251 | 252 | runner.assertEqual("", $('lorem').innerHTML, "innerHTML"); 253 | }, 254 | 255 | testExtractContents: function() { 256 | var runner = this; 257 | 258 | this.range.selectNodeContents($('lorem')); 259 | var contents = this.range.extractContents(); 260 | 261 | runner.assertEqual("", $('lorem').innerHTML, "innerHTML"); 262 | 263 | //IE document does not have any useful methods. Everyone else can just 264 | // read textContent, IE needs to append the fragment to another element 265 | // and read its innerHTML 266 | if (contents.textContent) { 267 | runner.assertEqual("Lorem ipsum", contents.textContent, "textContent"); 268 | } else { 269 | var e = new Element('div'); 270 | e.appendChild(contents); 271 | runner.assertEqual("Lorem ipsum", e.innerHTML, "textContent"); 272 | } 273 | }, 274 | 275 | testCloneContents: function() { 276 | var runner = this; 277 | 278 | this.range.selectNodeContents($('lorem')); 279 | var contents = this.range.cloneContents(); 280 | 281 | runner.assertEqual("Lorem ipsum", $('lorem').innerHTML, "innerHTML"); 282 | 283 | // IE document does not have any useful methods. Everyone else can just 284 | // read textContent, IE needs to append the fragment to another element 285 | // and read its innerHTML 286 | if (contents.textContent) { 287 | runner.assertEqual("Lorem ipsum", contents.textContent, "textContent"); 288 | } else { 289 | var e = new Element('div'); 290 | e.appendChild(contents); 291 | runner.assertEqual("Lorem ipsum", e.innerHTML, "textContent"); 292 | } 293 | }, 294 | 295 | testInsertNode: function() { 296 | var runner = this; 297 | 298 | var node = new Element('span', {id: 'inserted'}).update("inserted!"); 299 | 300 | this.range.selectNode($('lorem')); 301 | this.range.insertNode(node); 302 | 303 | runner.assertEqual("inserted!", $('inserted').innerHTML, "innerHTML"); 304 | 305 | runner.assertEqual($('content'), this.range.startContainer, "startContainer"); 306 | runner.assertEqual(0, this.range.startOffset, "startOffset"); 307 | runner.assertEqual($('content'), this.range.endContainer, "startContainer"); 308 | runner.assertEqual(2, this.range.endOffset, "endOffset"); 309 | runner.assertEqual(false, this.range.collapsed, "collapsed"); 310 | runner.assertEqual($('content'), this.range.commonAncestorContainer, "commonAncestorContainer"); 311 | }, 312 | 313 | testSurrondContents: function() { 314 | var runner = this; 315 | 316 | var node; 317 | 318 | node = new Element('span', {id: 'wrapper'}); 319 | 320 | this.range.selectNodeContents($('lorem')); 321 | this.range.surroundContents(node); 322 | 323 | expected = new Element('div'); 324 | expected.appendChild(new Element('span', {id: 'wrapper'}).update("Lorem ipsum")); 325 | 326 | runner.assertEqual(expected.innerHTML, $('lorem').innerHTML, "innerHTML"); 327 | }, 328 | 329 | testEqualRange: function() { 330 | var runner = this; 331 | 332 | if (!this.range.equalRange) { 333 | runner.flunk("equalRange is not implemented"); 334 | return false; 335 | } 336 | 337 | var r1 = document.createRange(); 338 | r1.selectNodeContents($('lorem')); 339 | 340 | var r2 = document.createRange(); 341 | r2.selectNodeContents($('lorem')); 342 | 343 | var r3 = document.createRange(); 344 | r3.selectNodeContents($('consectetuer')); 345 | 346 | runner.assert(r1.equalRange(r1), "r1.equalRange(r1)"); 347 | runner.assert(r2.equalRange(r2), "r2.equalRange(r2)"); 348 | runner.assert(r3.equalRange(r3), "r3.equalRange(r3)"); 349 | 350 | runner.assert(r1.equalRange(r2), "r1.equalRange(r2)"); 351 | runner.assert(r2.equalRange(r1), "r2.equalRange(r1)"); 352 | runner.assert(!r1.equalRange(r3), "r1.equalRange(r3)"); 353 | runner.assert(!r3.equalRange(r1), "r3.equalRange(r1)"); 354 | 355 | runner.assert(!r1.equalRange(null), "r1.equalRange(null)"); 356 | runner.assert(!r2.equalRange(null), "r2.equalRange(null)"); 357 | runner.assert(!r3.equalRange(null), "r3.equalRange(null)"); 358 | }, 359 | 360 | testGetNode: function() { 361 | var runner = this; 362 | 363 | if (!this.range.getNode) { 364 | runner.flunk("getNode is not implemented"); 365 | return false; 366 | } 367 | 368 | this.range.selectNodeContents($('lorem')); 369 | runner.assertEqual($('lorem'), this.range.getNode(), "getNode"); 370 | 371 | this.range.selectNode($('lorem')); 372 | runner.assertEqual($('lorem'), this.range.getNode(), "getNode"); 373 | 374 | this.range.setStart($('lorem'), 0); 375 | this.range.setEnd($('lorem'), 1); 376 | runner.assertEqual($('lorem'), this.range.getNode(), "getNode"); 377 | } 378 | }); 379 | -------------------------------------------------------------------------------- /test/unit/sanitize_contents_test.js: -------------------------------------------------------------------------------- 1 | function sanitize(html, options) { 2 | return new Element("div").update(html).sanitizeContents(options).innerHTML.toLowerCase(); 3 | } 4 | 5 | new Test.Unit.Runner({ 6 | testSanitize: function() { 7 | var runner = this; 8 | 9 | runner.assertEqual( 10 | "hello", 11 | sanitize("hello", {}) 12 | ); 13 | 14 | runner.assertEqual( 15 | "hello", 16 | sanitize("hello", {}) 17 | ); 18 | 19 | runner.assertEqual( 20 | "hello", 21 | sanitize("hello", {}) 22 | ); 23 | 24 | runner.assertEqual( 25 | "hello", 26 | sanitize("hello", {allow: "strong"}) 27 | ); 28 | 29 | runner.assertEqual( 30 | "hello", 31 | sanitize("hello", {allow: "strong"}) 32 | ); 33 | 34 | runner.assertEqual( 35 | "hello", 36 | sanitize("hello", {allow: "strong"}) 37 | ); 38 | 39 | runner.assertEqual( 40 | "hello world!", 41 | sanitize("

hello world!

", {allow: "strong"}) 42 | ); 43 | 44 | runner.assertEqual( 45 | "hello", 46 | sanitize("hello", {allow: "em, strong"}) 47 | ); 48 | 49 | runner.assertEqual( 50 | "google", 51 | sanitize("google", {allow: "a"}) 52 | ); 53 | 54 | runner.assertEqual( 55 | "google", 56 | sanitize("google", {allow: "a[href]"}) 57 | ); 58 | 59 | if (Prototype.Browser.IE) { 60 | runner.assertEqual( 61 | "hello ", 62 | sanitize("hello ", {allow: "span[id]"}) 63 | ); 64 | } else { 65 | runner.assertEqual( 66 | "hello ", 67 | sanitize("hello ", {allow: "span[id]"}) 68 | ); 69 | } 70 | 71 | runner.assertEqual( 72 | "", 73 | sanitize("", {allow: "img[src], a[href]"}) 74 | ); 75 | 76 | if (Prototype.Browser.Gecko) { 77 | var element; 78 | 79 | element = new Element("div").update('dirty formatting.
').sanitizeContents({skip: "[_moz_dirty]"}); 80 | runner.assertEqual( 81 | 'dirty formatting.
', 82 | element.innerHTML 83 | ); 84 | // _moz_dirty flag doesn't show up in innerHTML 85 | runner.assert(element.children[0].hasAttribute('_moz_dirty')); 86 | 87 | element = new Element("div").update('clean and dirty').sanitizeContents({skip: "[_moz_dirty]"}) 88 | runner.assertEqual( 89 | 'clean and dirty', 90 | element.innerHTML 91 | ); 92 | // _moz_dirty flag doesn't show up in innerHTML 93 | runner.assert(element.children[0].hasAttribute('_moz_dirty')); 94 | } 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /test/unit/selection_test.js: -------------------------------------------------------------------------------- 1 | new Test.Unit.Runner({ 2 | setup: function() { 3 | $('content').update( 4 | "Lorem ipsum dolor sit amet, " + 5 | "consectetuer adipiscing elit." 6 | ); 7 | 8 | this.selection = window.getSelection(); 9 | this.selection.removeAllRanges(); 10 | }, 11 | 12 | testSelectNode: function() { 13 | var runner = this; 14 | 15 | this.selection.selectNode($('lorem')); 16 | 17 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 18 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 19 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 20 | runner.assertEqual(11, this.selection.focusOffset, "focusOffset"); 21 | runner.assertEqual(false, this.selection.isCollapsed, "isCollapsed"); 22 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 23 | 24 | // Webkit extensions 25 | if (this.selection.baseNode) { 26 | runner.assertEqual("Lorem ipsum", this.selection.baseNode.textContent, "baseNode.textContent"); 27 | runner.assertEqual(0, this.selection.baseOffset, "baseOffset"); 28 | } 29 | if (this.selection.extentNode) { 30 | runner.assertEqual("Lorem ipsum", this.selection.extentNode.textContent, "extentNode.textContent"); 31 | runner.assertEqual(11, this.selection.extentOffset, "extentOffset"); 32 | } 33 | }, 34 | 35 | testCollapse: function() { 36 | var runner = this; 37 | 38 | this.selection.collapse($('lorem'), 0); 39 | 40 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 41 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 42 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 43 | runner.assertEqual(0, this.selection.focusOffset, "focusOffset"); 44 | runner.assertEqual(true, this.selection.isCollapsed, "isCollapsed"); 45 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 46 | }, 47 | 48 | testCollapseToStart: function() { 49 | var runner = this; 50 | 51 | range = document.createRange(); 52 | range.selectNodeContents($('lorem')); 53 | this.selection.addRange(range); 54 | this.selection.collapseToStart(); 55 | 56 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 57 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 58 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 59 | runner.assertEqual(0, this.selection.focusOffset, "focusOffset"); 60 | runner.assertEqual(true, this.selection.isCollapsed, "isCollapsed"); 61 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 62 | }, 63 | 64 | testCollapseToEnd: function() { 65 | var runner = this; 66 | 67 | range = document.createRange(); 68 | range.selectNodeContents($('lorem')); 69 | this.selection.addRange(range); 70 | this.selection.collapseToEnd(); 71 | 72 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 73 | runner.assertEqual(11, this.selection.anchorOffset, "anchorOffset"); 74 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 75 | runner.assertEqual(11, this.selection.focusOffset, "focusOffset"); 76 | runner.assertEqual(true, this.selection.isCollapsed, "isCollapsed"); 77 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 78 | }, 79 | 80 | testSelectAllChildren: function() { 81 | var runner = this; 82 | 83 | this.selection.selectAllChildren($('lorem')); 84 | 85 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 86 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 87 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 88 | runner.assertEqual(11, this.selection.focusOffset, "focusOffset"); 89 | runner.assertEqual(false, this.selection.isCollapsed, "isCollapsed"); 90 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 91 | 92 | this.selection.selectAllChildren($('content')); 93 | 94 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 95 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 96 | runner.assertEqual(" adipiscing elit.", this.selection.focusNode.textContent, "focusNode.textContent"); 97 | runner.assertEqual(17, this.selection.focusOffset, "focusOffset"); 98 | runner.assertEqual(false, this.selection.isCollapsed, "isCollapsed"); 99 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 100 | }, 101 | 102 | testDeleteFromDocument: function() { 103 | var runner = this; 104 | 105 | range = document.createRange(); 106 | range.selectNodeContents($('lorem')); 107 | this.selection.addRange(range); 108 | this.selection.deleteFromDocument(); 109 | 110 | runner.assertEqual("", $('lorem').innerHTML); 111 | }, 112 | 113 | testGetRangeAt: function() { 114 | var runner = this; 115 | 116 | range = document.createRange(); 117 | range.selectNodeContents($('lorem')); 118 | this.selection.addRange(range); 119 | range = this.selection.getRangeAt(0); 120 | 121 | runner.assertEqual(Node.TEXT_NODE, range.startContainer.nodeType, "startContainer.nodeType"); 122 | runner.assertEqual(null, range.startContainer.tagName, "startContainer.tagName"); 123 | runner.assertEqual(0, range.startOffset, "startOffset"); 124 | runner.assertEqual(Node.TEXT_NODE, range.endContainer.nodeType, "endContainer.nodeType"); 125 | runner.assertEqual(null, range.endContainer.tagName, "endContainer.tagName"); 126 | runner.assertEqual(11, range.endOffset, "endOffset"); 127 | runner.assertEqual(false, range.collapsed, "collapsed"); 128 | runner.assertEqual("Lorem ipsum", range.commonAncestorContainer.textContent, "commonAncestorContainer.textContent") 129 | }, 130 | 131 | testSelectFocusNode: function() { 132 | var runner = this; 133 | 134 | var range = document.createRange(); 135 | range.selectNodeContents($('lorem')); 136 | this.selection.addRange(range); 137 | this.selection.collapseToStart(); 138 | 139 | var range = document.createRange(); 140 | range.selectNode(this.selection.focusNode); 141 | this.selection.removeAllRanges(); 142 | this.selection.addRange(range); 143 | 144 | runner.assertEqual("Lorem ipsum", this.selection.anchorNode.textContent, "anchorNode.textContent"); 145 | runner.assertEqual(0, this.selection.anchorOffset, "anchorOffset"); 146 | runner.assertEqual("Lorem ipsum", this.selection.focusNode.textContent, "focusNode.textContent"); 147 | runner.assertEqual(11, this.selection.focusOffset, "focusOffset"); 148 | runner.assertEqual(false, this.selection.isCollapsed, "isCollapsed"); 149 | runner.assertEqual(1, this.selection.rangeCount, "rangeCount"); 150 | } 151 | }); 152 | -------------------------------------------------------------------------------- /test/unit/templates/default.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unit test file | <%= title %> | <%= template_name %> template | <%= timestamp %> 5 | 6 | 7 | 8 | 9 | 10 | <%#= lib_files %> 11 | 12 | 13 | <%= css_fixtures %> 14 | <%= js_fixtures %> 15 | <%= test_file %> 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | <%= html_fixtures %> 24 | 25 | 26 | --------------------------------------------------------------------------------