├── .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 |Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/examples/custom_toolbar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
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 | Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/examples/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/examples/toolbar_subclass.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Oops, you need to build the package before running this example. It's easy: just run rake
in the project's directory.
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 |
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 == "" + 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 |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 | 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/range.html: -------------------------------------------------------------------------------- 1 |Here is some basic text
\r\nwith a line break.
\r\nAnd maybe another paragraph
', 35 | getBrowserMarkupFrom('Some bold text
"', 39 | getBrowserMarkupFrom('Hello
World!
\n
\n') 193 | ); 194 | runner.assertEqual( 195 | '
\n') 197 | ); 198 | runner.assertEqual( 199 | '
\n
One
Two
\n\n\n
Break
\n') 201 | ); 202 | runner.assertEqual( 203 | '\n
Here is some basic text
with a line break.
And maybe another paragraph
\n') 205 | ); 206 | runner.assertEqual( 207 | '\n
Hello
World!
\n') 209 | ); 210 | runner.assertEqual( 211 | '\n
Hello
World!
Goodbye!
\n') 213 | ); 214 | runner.assertEqual( 215 | '\n
Hello
World!
Goodbye!
\n') 217 | ); 218 | runner.assertEqual( 219 | 'not
') 233 | ); 234 | runner.assertEqual( 235 | '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 | "