1 /*==============================================================================
2 Wikiwyg - Turn any HTML div into a wikitext /and/ wysiwyg edit area.
6 Wikiwyg is a Javascript library that can be easily integrated into any
7 wiki or blog software. It offers the user multiple ways to edit/view a
8 piece of content: Wysiwyg, Wikitext, Raw-HTML and Preview.
10 The library is easy to use, completely object oriented, configurable and
13 See the Wikiwyg documentation for details.
17 Brian Ingerson <ingy@cpan.org>
18 Casey West <casey@geeknest.com>
19 Chris Dent <cdent@burningchrome.com>
20 Matt Liggett <mml@pobox.com>
21 Ryan King <rking@panoptic.com>
22 Dave Rolsky <autarch@urth.org>
26 Copyright (c) 2005 Socialtext Corporation
28 Palo Alto, CA 94301 U.S.A.
31 Wikiwyg is free software.
33 This library is free software; you can redistribute it and/or modify it
34 under the terms of the GNU Lesser General Public License as published by
35 the Free Software Foundation; either version 2.1 of the License, or (at
36 your option) any later version.
38 This library is distributed in the hope that it will be useful, but
39 WITHOUT ANY WARRANTY; without even the implied warranty of
40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
41 General Public License for more details.
43 http://www.gnu.org/copyleft/lesser.txt
45 =============================================================================*/
47 /*==============================================================================
48 Subclass - this can be used to create new classes
49 =============================================================================*/
51 Subclass = function(name, base) {
52 if (!name) die("Can't create a subclass without a name");
54 var parts = name.split('.');
55 var subclass = window;
56 for (var i = 0; i < parts.length; i++) {
57 if (! subclass[parts[i]])
58 subclass[parts[i]] = function() {};
59 subclass = subclass[parts[i]];
63 var baseclass = eval('new ' + base + '()');
64 subclass.prototype = baseclass;
65 subclass.prototype.baseclass = base;
66 subclass.prototype.superfunc = Subclass.generate_superfunc();
68 subclass.prototype.classname = name;
69 return subclass.prototype;
72 Subclass.generate_superfunc = function() {
73 return function(func) {
75 for (var b = this.baseclass; b; b = p.baseclass) {
76 p = eval(b + '.prototype');
77 if (p[func] && p[func] != this[func])
81 "No superfunc function for: " + func + "\n" +
82 "baseclass was: " + this.baseclass + "\n" +
83 "caller was: " + arguments.callee.caller
88 /*==============================================================================
89 Wikiwyg - Primary Wikiwyg base class
90 =============================================================================*/
92 // Constructor and class methods
93 proto = new Subclass('Wikiwyg');
95 Wikiwyg.VERSION = '0.12';
97 // Browser support properties
98 Wikiwyg.ua = navigator.userAgent.toLowerCase();
100 Wikiwyg.ua.indexOf("msie") != -1 &&
101 Wikiwyg.ua.indexOf("opera") == -1 &&
102 Wikiwyg.ua.indexOf("webtv") == -1
105 Wikiwyg.ua.indexOf('gecko') != -1 &&
106 Wikiwyg.ua.indexOf('safari') == -1
108 Wikiwyg.is_safari = (
109 Wikiwyg.ua.indexOf('safari') != -1
112 Wikiwyg.ua.indexOf('opera') != -1
114 Wikiwyg.browserIsSupported = (
119 // Wikiwyg environment setup public methods
120 proto.createWikiwygArea = function(div, config) {
121 this.set_config(config);
122 this.initializeObject(div, config);
126 javascriptLocation: 'Wikiwyg/',
127 doubleClickToEdit: false,
128 toolbarClass: 'Wikiwyg.Toolbar',
135 proto.initializeObject = function(div, config) {
136 if (! Wikiwyg.browserIsSupported) {
137 alert('Your browser is not supported yet. The toolbar will not be activated.')
140 if (this.enabled) return;
143 this.divHeight = this.div.offsetHeight;
144 if (!config) config = {};
146 this.mode_objects = {};
147 for (var i = 0; i < this.config.modeClasses.length; i++) {
148 var class_name = this.config.modeClasses[i];
149 var mode_object = eval('new ' + class_name + '()');
150 mode_object.wikiwyg = this;
151 mode_object.set_config(config[mode_object.classtype]);
152 mode_object.initializeObject();
153 this.mode_objects[class_name] = mode_object;
154 if (! this.first_mode) {
155 this.first_mode = mode_object;
159 if (this.config.toolbarClass) {
160 var class_name = this.config.toolbarClass;
161 this.toolbarObject = eval('new ' + class_name + '()');
162 this.toolbarObject.wikiwyg = this;
163 this.toolbarObject.set_config(config.toolbar);
164 this.toolbarObject.initializeObject();
165 this.placeToolbar(this.toolbarObject.div);
168 // These objects must be _created_ before the toolbar is created
169 // but _inserted_ after.
170 for (var i = 0; i < this.config.modeClasses.length; i++) {
171 var mode_class = this.config.modeClasses[i];
172 var mode_object = this.mode_objects[mode_class];
173 this.insert_div_before(mode_object.div);
176 if (this.config.doubleClickToEdit) {
178 this.div.ondblclick = function() { self.editMode() };
182 proto.placeToolbar = function(div) {
183 this.insert_div_before(div);
186 // Wikiwyg environment setup private methods
187 proto.set_config = function(user_config) {
188 for (var key in this.config)
189 if (user_config && user_config[key])
190 this.config[key] = user_config[key];
191 else if (this[key] != null)
192 this.config[key] = this[key];
195 proto.insert_div_before = function(div) {
196 div.style.display = 'none';
197 if (! div.iframe_hack) {
198 this.div.parentNode.insertBefore(div, this.div);
202 // Wikiwyg actions - public interface methods
203 proto.saveChanges = function() {
204 alert('Wikiwyg.prototype.saveChanges not subclassed');
207 // Wikiwyg actions - public methods
208 proto.editMode = function() { // See IE, below
209 this.current_mode = this.first_mode;
210 //this.current_mode.fromHtml(this.div.innerHTML);
211 this.current_mode.fromWikitext(this.div.value);
212 this.toolbarObject.resetModeSelector();
213 this.current_mode.enableThis();
216 proto.displayMode = function() {
217 for (var i = 0; i < this.config.modeClasses.length; i++) {
218 var mode_class = this.config.modeClasses[i];
219 var mode_object = this.mode_objects[mode_class];
220 mode_object.disableThis();
222 this.toolbarObject.disableThis();
223 this.div.style.display = 'block';
224 this.divHeight = this.div.offsetHeight;
227 proto.switchMode = function(new_mode_key) {
228 var new_mode = this.mode_objects[new_mode_key];
229 var old_mode = this.current_mode;
231 if(new_mode == old_mode)
234 //Set cookie to keep last editing mode
235 //document.cookie = "Mode="+new_mode_key;
238 new_mode.enableStarted();
239 old_mode.disableStarted();
242 self.previous_mode = old_mode;
243 new_mode.fromHtml(html);
244 old_mode.disableThis();
245 new_mode.enableThis();
246 new_mode.enableFinished();
247 old_mode.disableFinished();
248 self.current_mode = new_mode;
253 proto.cancelEdit = function() {
257 proto.fromHtml = function(html) {
258 this.div.innerHTML = html;
261 // Class level helper methods
262 Wikiwyg.unique_id_base = 0;
263 Wikiwyg.createUniqueId = function() {
264 return 'wikiwyg_' + Wikiwyg.unique_id_base++;
267 Wikiwyg.liveUpdate = function(method, url, query, callback) {
268 var req = new XMLHttpRequest();
271 url = url + '?' + query;
274 req.open(method, url);
275 req.onreadystatechange = function() {
276 if (req.readyState == 4 && req.status == 200)
277 callback(req.responseText);
279 if (method == 'POST') {
280 req.setRequestHeader(
282 'application/x-www-form-urlencoded'
288 Wikiwyg.htmlUnescape = function(escaped) {
289 // XXX Need a better way to unescape all entities
291 .replace(/&/g, '&')
292 .replace(/</g, '<')
293 .replace(/>/g, '>');
296 Wikiwyg.showById = function(id) {
297 document.getElementById(id).style.visibility = 'inherit';
300 Wikiwyg.hideById = function(id) {
301 document.getElementById(id).style.visibility = 'hidden';
305 Wikiwyg.changeLinksMatching = function(attribute, pattern, func) {
306 var links = document.getElementsByTagName('a');
307 for (var i = 0; i < links.length; i++) {
309 var my_attribute = link.getAttribute(attribute);
310 if (my_attribute && my_attribute.match(pattern)) {
311 link.setAttribute('href', '#');
317 Wikiwyg.createElementWithAttrs = function(element, attrs, doc) {
320 return Wikiwyg.create_element_with_attrs(element, attrs, doc);
324 Wikiwyg.create_element_with_attrs = function(element, attrs, doc) {
325 var elem = doc.createElement(element);
327 elem.setAttribute(name, attrs[name]);
331 die = function(e) { // See IE, below
335 String.prototype.times = function(n) {
336 return n ? this + this.times(n-1) : "";
339 /*==============================================================================
340 Base class for Wikiwyg classes
341 =============================================================================*/
342 proto = new Subclass('Wikiwyg.Base');
344 proto.set_config = function(user_config) {
345 for (var key in this.config) {
346 if (user_config != null && user_config[key] != null)
347 this.merge_config(key, user_config[key]);
348 else if (this[key] != null)
349 this.merge_config(key, this[key]);
350 else if (this.wikiwyg.config[key] != null)
351 this.merge_config(key, this.wikiwyg.config[key]);
355 proto.merge_config = function(key, value) {
356 if (value instanceof Array) {
357 this.config[key] = value;
359 // cross-browser RegExp object check
360 else if (typeof value.test == 'function') {
361 this.config[key] = value;
363 else if (value instanceof Object) {
364 if (!this.config[key])
365 this.config[key] = {};
366 for (var subkey in value) {
367 this.config[key][subkey] = value[subkey];
371 this.config[key] = value;
375 /*==============================================================================
376 Base class for Wikiwyg Mode classes
377 =============================================================================*/
378 proto = new Subclass('Wikiwyg.Mode', 'Wikiwyg.Base');
380 proto.enableThis = function() {
381 this.div.style.display = 'block';
382 this.display_unsupported_toolbar_buttons('none');
383 this.wikiwyg.toolbarObject.enableThis();
384 this.wikiwyg.div.style.display = 'none';
387 proto.display_unsupported_toolbar_buttons = function(display) {
388 if (!this.config) return;
389 var disabled = this.config.disabledToolbarButtons;
390 if (!disabled || disabled.length < 1) return;
392 var toolbar_div = this.wikiwyg.toolbarObject.div;
393 var toolbar_buttons = toolbar_div.childNodes;
394 for (var i in disabled) {
395 var action = disabled[i];
397 for (var i in toolbar_buttons) {
398 var button = toolbar_buttons[i];
399 var src = button.src;
402 if (src.match(action)) {
403 button.style.display = display;
410 proto.enableStarted = function() {}
411 proto.enableFinished = function() {}
412 proto.disableStarted = function() {}
413 proto.disableFinished = function() {}
415 proto.disableThis = function() {
416 this.display_unsupported_toolbar_buttons('inline');
417 this.div.style.display = 'none';
420 proto.process_command = function(command) {
421 if (this['do_' + command])
422 this['do_' + command](command);
425 proto.enable_keybindings = function() { // See IE
426 if (!this.key_press_function) {
427 this.key_press_function = this.get_key_press_function();
428 this.get_keybinding_area().addEventListener(
429 'keypress', this.key_press_function, true
434 proto.get_key_press_function = function() {
437 if (! e.ctrlKey) return;
438 var key = String.fromCharCode(e.charCode).toLowerCase();
441 case 'b': command = 'bold'; break;
442 case 'i': command = 'italic'; break;
443 case 'u': command = 'underline'; break;
444 case 'd': command = 'strike'; break;
445 case 'l': command = 'link'; break;
451 self.process_command(command);
456 proto.get_edit_height = function() {
457 var height = parseInt(
458 this.wikiwyg.divHeight *
459 this.config.editHeightAdjustment
461 var min = this.config.editHeightMinimum;
467 proto.setHeightOf = function(elem) {
468 elem.height = this.get_edit_height() + 'px';
471 /*==============================================================================
472 Support for Internet Explorer in Wikiwyg
473 =============================================================================*/
476 Wikiwyg.create_element_with_attrs = function(element, attrs, doc) {
478 // Note the double-quotes (make sure your data doesn't use them):
480 str += ' ' + name + '="' + attrs[name] + '"';
481 return doc.createElement('<' + element + str + '>');
489 proto = Wikiwyg.Mode.prototype;
491 proto.enable_keybindings = function() {}
493 } // end of global if statement for IE overrides