Revision: 5535
Author: bodo
Date: 2008-10-23 14:48:08 -0400 (Thu, 23 Oct 2008)
Log Message:
-----------
Added a new WYSIWYG elementclass Krang::Element::PoorText and all it takes.
Modified Paths:
--------------
trunk/krang/element_lib/TestSet1/page.pm
trunk/krang/t/publish/page.tmpl
trunk/krang/templates/header.base.tmpl
Added Paths:
-----------
trunk/krang/htdocs/js/dragdrop.js
trunk/krang/htdocs/js/effects.js
trunk/krang/htdocs/poortext/
trunk/krang/htdocs/poortext/images/
trunk/krang/htdocs/poortext/images/button_sprite.png
trunk/krang/htdocs/poortext/images/ldquo.png
trunk/krang/htdocs/poortext/images/lsquo.png
trunk/krang/htdocs/poortext/images/mdash.png
trunk/krang/htdocs/poortext/images/ndash.png
trunk/krang/htdocs/poortext/images/rdquo.png
trunk/krang/htdocs/poortext/images/rsquo.png
trunk/krang/htdocs/poortext/images/special_char_sprite.png
trunk/krang/htdocs/poortext/poortext.css
trunk/krang/htdocs/poortext/poortext.js
trunk/krang/htdocs/poortext/poortext_gecko.js
trunk/krang/htdocs/poortext/poortext_ie.js
trunk/krang/htdocs/poortext/poortext_webkit.js
trunk/krang/lib/Krang/ElementClass/PoorText.pm
Modified: trunk/krang/element_lib/TestSet1/page.pm
===================================================================
--- trunk/krang/element_lib/TestSet1/page.pm 2008-10-23 17:52:13 UTC (rev 5534)
+++ trunk/krang/element_lib/TestSet1/page.pm 2008-10-23 18:48:08 UTC (rev 5535)
@@ -18,6 +18,8 @@
use Krang::ClassLoader base => 'ElementClass';
+use Krang::ElementClass::PoorText;
+
sub new {
my $pkg = shift;
my %args = (
@@ -123,6 +125,21 @@
return $data;
},
),
+ pkg('ElementClass::PoorText')->new(
+ name => 'poortext_header',
+ type => 'text',
+ commands => 'basic_with_special_chars',
+ command_button_bar => 1,
+ special_char_bar => 0,
+ ),
+ pkg('ElementClass::PoorText')->new(
+ name => 'poortext_paragraph',
+ type => 'textarea',
+ commands => 'all',
+ required => 1,
+ command_button_bar => 1,
+ special_char_bar => 0,
+ ),
],
@_
);
Added: trunk/krang/htdocs/js/dragdrop.js
===================================================================
--- trunk/krang/htdocs/js/dragdrop.js (rev 0)
+++ trunk/krang/htdocs/js/dragdrop.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,944 @@
+// script.aculo.us dragdrop.js v1.7.0, Fri Jan 19 19:16:36 CET 2007
+
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@...)
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(typeof Effect == 'undefined')
+ throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+var Droppables = {
+ drops: [],
+
+ remove: function(element) {
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+ },
+
+ add: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null,
+ tree: false
+ }, arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = [];
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ containment.each( function(c) { options._containers.push($(c)) });
+ } else {
+ options._containers.push($(containment));
+ }
+ }
+
+ if(options.accept) options.accept = [options.accept].flatten();
+
+ Element.makePositioned(element); // fix IE
+ options.element = element;
+
+ this.drops.push(options);
+ },
+
+ findDeepestChild: function(drops) {
+ deepest = drops[0];
+
+ for (i = 1; i < drops.length; ++i)
+ if (Element.isParent(drops[i].element, deepest.element))
+ deepest = drops[i];
+
+ return deepest;
+ },
+
+ isContained: function(element, drop) {
+ var containmentNode;
+ if(drop.tree) {
+ containmentNode = element.treeNode;
+ } else {
+ containmentNode = element.parentNode;
+ }
+ return drop._containers.detect(function(c) { return containmentNode == c });
+ },
+
+ isAffected: function(point, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ Position.within(drop.element, point[0], point[1]) );
+ },
+
+ deactivate: function(drop) {
+ if(drop.hoverclass)
+ Element.removeClassName(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(drop.hoverclass)
+ Element.addClassName(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ },
+
+ show: function(point, element) {
+ if(!this.drops.length) return;
+ var affected = [];
+
+ if(this.last_active) this.deactivate(this.last_active);
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0) {
+ drop = Droppables.findDeepestChild(affected);
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ Droppables.activate(drop);
+ }
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop)
+ this.last_active.onDrop(element, this.last_active.element, event);
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ if(draggable.options.delay) {
+ this._timeout = setTimeout(function() {
+ Draggables._timeout = null;
+ window.focus();
+ Draggables.activeDraggable = draggable;
+ }.bind(this), draggable.options.delay);
+ } else {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ }
+ },
+
+ deactivate: function() {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ this.activeDraggable = null;
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable._dragging = {};
+
+Draggable.prototype = {
+ initialize: function(element) {
+ var defaults = {
+ handle: false,
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+ queue: {scope:'_draggable', position:'end'}
+ });
+ },
+ endeffect: function(element) {
+ var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
+ queue: {scope:'_draggable', position:'end'},
+ afterFinish: function(){
+ Draggable._dragging[element] = false
+ }
+ });
+ },
+ zindex: 1000,
+ revert: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ delay: 0
+ };
+
+ if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+ Object.extend(defaults, {
+ starteffect: function(element) {
+ element._opacity = Element.getOpacity(element);
+ Draggable._dragging[element] = true;
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
+ }
+ });
+
+ var options = Object.extend(defaults, arguments[1] || {});
+
+ this.element = $(element);
+
+ if(options.handle && (typeof options.handle == 'string'))
+ this.handle = this.element.down('.'+options.handle, 0);
+
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+ options.scroll = $(options.scroll);
+ this._isScrollChild = Element.childOf(this.element, options.scroll);
+ }
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.delta = this.currentDelta();
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(Element.getStyle(this.element,'left') || '0'),
+ parseInt(Element.getStyle(this.element,'top') || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(typeof Draggable._dragging[this.element] != 'undefined' &&
+ Draggable._dragging[this.element]) return;
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if((tag_name = src.tagName.toUpperCase()) && (
+ tag_name=='INPUT' ||
+ tag_name=='SELECT' ||
+ tag_name=='OPTION' ||
+ tag_name=='BUTTON' ||
+ tag_name=='TEXTAREA')) return;
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ if(this.options.scroll) {
+ if (this.options.scroll == window) {
+ var where = this._getWindowScroll(this.options.scroll);
+ this.originalScrollLeft = where.left;
+ this.originalScrollTop = where.top;
+ } else {
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
+ this.originalScrollTop = this.options.scroll.scrollTop;
+ }
+ }
+
+ Draggables.notify('onStart', this, event);
+
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ Draggables.notify('onDrag', this, event);
+
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ if(this.options.scroll) {
+ this.stopScrolling();
+
+ var p;
+ if (this.options.scroll == window) {
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+ } else {
+ p = Position.page(this.options.scroll);
+ p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+ p[1] += this.options.scroll.scrollTop + Position.deltaY;
+ p.push(p[0]+this.options.scroll.offsetWidth);
+ p.push(p[1]+this.options.scroll.offsetHeight);
+ }
+ var speed = [0,0];
+ if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.ghosting) {
+ Position.relativize(this.element);
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ if(success) Droppables.fire(event, this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && typeof revert == 'function') revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(event.keyCode!=Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.stopScrolling();
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ if(this.options.ghosting) {
+ var r = Position.realOffset(this.element);
+ pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+ }
+
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(typeof this.options.snap == 'function') {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(this.options.snap instanceof Array) {
+ p = p.map( function(v, i) {
+ return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {
+ return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+
+ stopScrolling: function() {
+ if(this.scrollInterval) {
+ clearInterval(this.scrollInterval);
+ this.scrollInterval = null;
+ Draggables._lastScrollPointer = null;
+ }
+ },
+
+ startScrolling: function(speed) {
+ if(!(speed[0] || speed[1])) return;
+ this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+ this.lastScrolled = new Date();
+ this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+ },
+
+ scroll: function() {
+ var current = new Date();
+ var delta = current - this.lastScrolled;
+ this.lastScrolled = current;
+ if(this.options.scroll == window) {
+ with (this._getWindowScroll(this.options.scroll)) {
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+ var d = delta / 1000;
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+ }
+ }
+ } else {
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
+ }
+
+ Position.prepare();
+ Droppables.show(Draggables._lastPointer, this.element);
+ Draggables.notify('onDrag', this);
+ if (this._isScrollChild) {
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+ if (Draggables._lastScrollPointer[0] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 0)
+ Draggables._lastScrollPointer[1] = 0;
+ this.draw(Draggables._lastScrollPointer);
+ }
+
+ if(this.options.change) this.options.change(this);
+ },
+
+ _getWindowScroll: function(w) {
+ var T, L, W, H;
+ with (w.document) {
+ if (w.document.documentElement && documentElement.scrollTop) {
+ T = documentElement.scrollTop;
+ L = documentElement.scrollLeft;
+ } else if (w.document.body) {
+ T = body.scrollTop;
+ L = body.scrollLeft;
+ }
+ if (w.innerWidth) {
+ W = w.innerWidth;
+ H = w.innerHeight;
+ } else if (w.document.documentElement && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+var Sortable = {
+ SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+
+ sortables: {},
+
+ _findRootElement: function(element) {
+ while (element.tagName.toUpperCase() != "BODY") {
+ if(element.id && Sortable.sortables[element.id]) return element;
+ element = element.parentNode;
+ }
+ },
+
+ options: function(element) {
+ element = Sortable._findRootElement($(element));
+ if(!element) return;
+ return Sortable.sortables[element.id];
+ },
+
+ destroy: function(element){
+ var s = Sortable.options(element);
+
+ if(s) {
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+
+ delete Sortable.sortables[s.element.id];
+ }
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false,
+ treeTag: 'ul',
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ delay: 0,
+ hoverclass: null,
+ ghosting: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: this.SERIALIZE_RULE,
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || {});
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ scroll: options.scroll,
+ scrollSpeed: options.scrollSpeed,
+ scrollSensitivity: options.scrollSensitivity,
+ delay: options.delay,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ tree: options.tree,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover
+ }
+
+ var options_for_tree = {
+ onHover: Sortable.onEmptyHover,
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // drop on empty handling
+ if(options.dropOnEmpty || options.tree) {
+ Droppables.add(element, options_for_tree);
+ options.droppables.push(element);
+ }
+
+ (this.findElements(element, options) || []).each( function(e) {
+ // handles are per-draggable
+ var handle = options.handle ?
+ $(e).down('.'+options.handle,0) : e;
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ if(options.tree) e.treeNode = element;
+ options.droppables.push(e);
+ });
+
+ if(options.tree) {
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
+ Droppables.add(e, options_for_tree);
+ e.treeNode = element;
+ options.droppables.push(e);
+ });
+ }
+
+ // keep reference
+ this.sortables[element.id] = options;
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.tag);
+ },
+
+ findTreeElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.treeTag);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(Element.isParent(dropon, element)) return;
+
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, overlap) {
+ var oldParentNode = element.parentNode;
+ var droponOptions = Sortable.options(dropon);
+
+ if(!Element.isParent(dropon, element)) {
+ var index;
+
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+ var child = null;
+
+ if(children) {
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+
+ for (index = 0; index < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < children.length ? children[index + 1] : null;
+ break;
+ } else {
+ child = children[index];
+ break;
+ }
+ }
+ }
+
+ dropon.insertBefore(element, child);
+
+ Sortable.options(oldParentNode).onChange(element);
+ droponOptions.onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Sortable._marker.hide();
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker =
+ ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+ hide().addClassName('dropmarker').setStyle({position:'absolute'});
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+ else
+ Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+
+ Sortable._marker.show();
+ },
+
+ _tree: function(element, options, parent) {
+ var children = Sortable.findElements(element, options) || [];
+
+ for (var i = 0; i < children.length; ++i) {
+ var match = children[i].id.match(options.format);
+
+ if (!match) continue;
+
+ var child = {
+ id: encodeURIComponent(match ? match[1] : null),
+ element: element,
+ parent: parent,
+ children: [],
+ position: parent.children.length,
+ container: $(children[i]).down(options.treeTag)
+ }
+
+ /* Get the element containing the children and recurse over it */
+ if (child.container)
+ this._tree(child.container, options, child)
+
+ parent.children.push (child);
+ }
+
+ return parent;
+ },
+
+ tree: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ treeTag: sortableOptions.treeTag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format
+ }, arguments[1] || {});
+
+ var root = {
+ id: null,
+ parent: null,
+ children: [],
+ container: element,
+ position: 0
+ }
+
+ return Sortable._tree(element, options, root);
+ },
+
+ /* Construct a [i] index for a particular node */
+ _constructIndex: function(node) {
+ var index = '';
+ do {
+ if (node.id) index = '[' + node.position + ']' + index;
+ } while ((node = node.parent) != null);
+ return index;
+ },
+
+ sequence: function(element) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[1] || {});
+
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+ });
+ },
+
+ setSequence: function(element, new_sequence) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[2] || {});
+
+ var nodeMap = {};
+ this.findElements(element, options).each( function(n) {
+ if (n.id.match(options.format))
+ nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+ n.parentNode.removeChild(n);
+ });
+
+ new_sequence.each(function(ident) {
+ var n = nodeMap[ident];
+ if (n) {
+ n[1].appendChild(n[0]);
+ delete nodeMap[ident];
+ }
+ });
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var options = Object.extend(Sortable.options(element), arguments[1] || {});
+ var name = encodeURIComponent(
+ (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+
+ if (options.tree) {
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
+ return [name + Sortable._constructIndex(item) + "[id]=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+}
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+ if (!child.parentNode || child == element) return false;
+ if (child.parentNode == element) return true;
+ return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {
+ if(!element.hasChildNodes()) return null;
+ tagName = tagName.toUpperCase();
+ if(only) only = [only].flatten();
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+ elements.push(e);
+ if(recursive) {
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+ return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+}
Added: trunk/krang/htdocs/js/effects.js
===================================================================
--- trunk/krang/htdocs/js/effects.js (rev 0)
+++ trunk/krang/htdocs/js/effects.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,1090 @@
+// script.aculo.us effects.js v1.7.0, Fri Jan 19 19:16:36 CET 2007
+
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if(this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if(this.slice(0,1) == '#') {
+ if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if(this.length==7) color = this.toLowerCase();
+ }
+ }
+ return(color.length==7 ? color : (arguments[0] || this));
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+ return element;
+}
+
+Element.getOpacity = function(element){
+ return $(element).getStyle('opacity');
+}
+
+Element.setOpacity = function(element, value){
+ return $(element).setStyle({opacity:value});
+}
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+}
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+ var args = arguments;
+ this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ tagifyText: function(element) {
+ if(typeof Builder == 'undefined')
+ throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
+
+ var tagifyStyle = 'position:relative';
+ if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if(child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ Builder.node('span',{style: tagifyStyle},
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if(((typeof element == 'object') ||
+ (typeof element == 'function')) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || {});
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || {});
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+ },
+ pulse: function(pos, pulses) {
+ pulses = pulses || 5;
+ return (
+ Math.round((pos % (1/pulses)) * pulses) == 0 ?
+ ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
+ 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
+ );
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+};
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = (typeof effect.options.queue == 'string') ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if(!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if(this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ for(var i=0, len=this.effects.length;i<len;i++)
+ if(this.effects[i]) this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if(typeof queueName != 'string') return queueName;
+
+ if(!this.instances[queueName])
+ this.instances[queueName] = new Effect.ScopedQueue();
+
+ return this.instances[queueName];
+ }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 60.0, // max. 60fps due to Effect.Queue implementation
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ position: null,
+ start: function(options) {
+ this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn + (this.options.duration*1000);
+ this.event('beforeStart');
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if(timePos >= this.startOn) {
+ if(timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if(this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
+ var frame = Math.round(pos * this.options.fps * this.options.duration);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ render: function(pos) {
+ if(this.state == 'idle') {
+ this.state = 'running';
+ this.event('beforeSetup');
+ if(this.setup) this.setup();
+ this.event('afterSetup');
+ }
+ if(this.state == 'running') {
+ if(this.options.transition) pos = this.options.transition(pos);
+ pos *= (this.options.to-this.options.from);
+ pos += this.options.from;
+ this.position = pos;
+ this.event('beforeUpdate');
+ if(this.update) this.update(pos);
+ this.event('afterUpdate');
+ }
+ },
+ cancel: function() {
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if(this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ var data = $H();
+ for(property in this)
+ if(typeof this[property] != 'function') data[property] = this[property];
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if(effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Event = Class.create();
+Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+ initialize: function() {
+ var options = Object.extend({
+ duration: 0
+ }, arguments[0] || {});
+ this.start(options);
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Bug in Opera: Opera returns the "real" position of a static element or
+ // relative element that does not have top/left explicitly set.
+ // ==> Always set top and left for position relative elements in your stylesheets
+ // (to 0 if you do not need them)
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if(this.options.mode == 'absolute') {
+ // absolute movement, so we need to calc deltaX and deltaY
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: Math.round(this.options.x * position + this.originalLeft) + 'px',
+ top: Math.round(this.options.y * position + this.originalTop) + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || {});
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = {};
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if(fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if(this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if(/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if(!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = {};
+ if(this.options.scaleX) d.width = Math.round(width) + 'px';
+ if(this.options.scaleY) d.height = Math.round(height) + 'px';
+ if(this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if(this.elementPositioning == 'absolute') {
+ if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if(this.options.scaleY) d.top = -topd + 'px';
+ if(this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = {};
+ if (!this.options.keepBackgroundImage) {
+ this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+ this.element.setStyle({backgroundImage: 'none'});
+ }
+ if(!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if(!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ this.start(arguments[1] || {});
+ },
+ setup: function() {
+ Position.prepare();
+ var offsets = Position.cumulativeOffset(this.element);
+ if(this.options.offset) offsets[1] += this.options.offset;
+ var max = window.innerHeight ?
+ window.height - window.innerHeight :
+ document.body.scrollHeight -
+ (document.documentElement.clientHeight ?
+ document.documentElement.clientHeight : document.body.clientHeight);
+ this.scrollStart = Position.deltaY;
+ this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+ },
+ update: function(position) {
+ Position.prepare();
+ window.scrollTo(Position.deltaX,
+ this.scrollStart + (position*this.delta));
+ }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if(effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element)
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ })
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ restoreAfterFinish: true,
+ beforeStartInternal: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
+ effect.element.down().undoPositioned();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ )
+ }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+}
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+ var oldOpacity = element.getInlineOpacity();
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || {}));
+};
+
+Effect.Morph = Class.create();
+Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: {}
+ }, arguments[1] || {});
+ if (typeof options.style == 'string') {
+ if(options.style.indexOf(':') == -1) {
+ var cssText = '', selector = '.' + options.style;
+ $A(document.styleSheets).reverse().each(function(styleSheet) {
+ if (styleSheet.cssRules) cssRules = styleSheet.cssRules;
+ else if (styleSheet.rules) cssRules = styleSheet.rules;
+ $A(cssRules).reverse().each(function(rule) {
+ if (selector == rule.selectorText) {
+ cssText = rule.style.cssText;
+ throw $break;
+ }
+ });
+ if (cssText) throw $break;
+ });
+ this.style = cssText.parseStyle();
+ options.afterFinishInternal = function(effect){
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ if(transform.style != 'opacity')
+ effect.element.style[transform.style.camelize()] = '';
+ });
+ }
+ } else this.style = options.style.parseStyle();
+ } else this.style = $H(options.style)
+ this.start(options);
+ },
+ setup: function(){
+ function parseColor(color){
+ if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 )
+ });
+ }
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0].underscore().dasherize(), value = pair[1], unit = null;
+
+ if(value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if(property == 'opacity') {
+ value = parseFloat(value);
+ if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if(Element.CSS_LENGTH.test(value))
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
+ value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
+
+ var originalValue = this.element.getStyle(property);
+ return $H({
+ style: property,
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ });
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ )
+ });
+ },
+ update: function(position) {
+ var style = $H(), value = null;
+ this.transforms.each(function(transform){
+ value = transform.unit=='color' ?
+ $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(transform.originalValue[i]+
+ (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) :
+ transform.originalValue + Math.round(
+ ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
+ style[transform.style] = value;
+ });
+ this.element.setStyle(style);
+ }
+});
+
+Effect.Transform = Class.create();
+Object.extend(Effect.Transform.prototype, {
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || {};
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ var data = $H(track).values().first();
+ this.tracks.push($H({
+ ids: $H(track).keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var elements = [$(track.ids) || $$(track.ids)].flatten();
+ return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.prototype.parseStyle = function(){
+ var element = Element.extend(document.createElement('div'));
+ element.innerHTML = '<div style="' + this + '"></div>';
+ var style = element.down().style, styleRules = $H();
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if(style[property]) styleRules[property] = style[property];
+ });
+ if(/MSIE/.test(navigator.userAgent) && !window.opera && this.indexOf('opacity') > -1) {
+ styleRules.opacity = this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1];
+ }
+ return styleRules;
+};
+
+Element.morph = function(element, style) {
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
+ return element;
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
+ function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+ s = effect.gsub(/_/, '-').camelize();
+ effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[effect_class](element, options);
+ return $(element);
+};
+
+Element.addMethods();
\ No newline at end of file
Added: trunk/krang/htdocs/poortext/images/button_sprite.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/button_sprite.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/ldquo.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/ldquo.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/lsquo.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/lsquo.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/mdash.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/mdash.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/ndash.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/ndash.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/rdquo.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/rdquo.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/rsquo.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/rsquo.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/images/special_char_sprite.png
===================================================================
(Binary files differ)
Property changes on: trunk/krang/htdocs/poortext/images/special_char_sprite.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: trunk/krang/htdocs/poortext/poortext.css
===================================================================
--- trunk/krang/htdocs/poortext/poortext.css (rev 0)
+++ trunk/krang/htdocs/poortext/poortext.css 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,264 @@
+/* The DIV to make editable */
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+.poortext {
+ background-color: #ffffff !important;
+ border: 1px solid silver;
+ font-size: 12px !important;
+ font-family: sans-serif;
+ overflow: auto;
+ color: #000000;
+}
+
+.poortext a {
+ color: #0000ff;
+ text-decoration: underline;
+ font-size: 12px !important;
+ font-weight: normal;
+ font-family: sans-serif;
+}
+
+.pt-text {
+/*
+ Ratio between font-size, height, line-height and padding is important
+ to mimic text flavor, to ensure that tails of letters in the
+ previous line (in Gecko there are really multiple lines )don't show
+ up in the current line and to make sure the text doesn't move
+ vertically when focusing and beginning to type.
+*/
+ font-size: 14px;
+ height: 26px; /* for IE6 */
+ line-height: 26px; /* must be much bigger than font-size */
+ overflow: hidden;
+ padding: 0px 0px 6px 0px; /* push it up */
+ width: 300px;
+}
+
+html>body .pt-text {
+ height: 16px; /* for Gecko */
+ line-height: 22px; /* for Gecko */
+}
+
+/* The textarea to show the source */
+.pta {
+ width: 400px;
+ height: 200px;
+ border: 1px solid silver;
+}
+/* Dialog styles */
+.pt-popup {
+ color: #000000;
+ position: fixed;
+ cursor: move;
+/* overflow: auto; */
+
+/* Required for Gecko < 2.0 (1.9) see bug #167801 overflow:auto works
+good for FF>1.5 But with FF 1.0.4 it shows the scroll bars if width
+and height are not set So we use the other workaround with
+position:fixed But that doesn't play nicely with draggables, so we
+revert to overflow, but we set it directly on a parent DIV of the text
+input fields. */
+
+}
+
+.pt-popup-content {
+ font-size: 12px;
+ font-family: Arial, Verdana, sans-serif;
+ background-color: #FFFFE0;
+ padding: 16px 14px 14px 14px;
+ border: 1px solid #6A5ACD;
+}
+
+.pt-fieldset {
+ border: 1px solid #333333;
+ padding: 10px 8px;
+}
+
+.pt-fieldset td {
+ font-size: 12px;
+}
+
+.pt-legend {
+ position: absolute;
+ top: 9px;
+ left: 25px;
+ background-color: #FFFFE0;
+ padding: 2px;
+}
+
+.pt-dlg-form {
+ margin: 0;
+ padding: 0;
+}
+
+.pt-dlg-first-col {
+ width: 30px;
+ cursor: default;
+}
+
+.pt-dlg-second-col {
+ cursor: default;
+/* nothing yet */
+}
+
+.pt-dlg-item {
+ padding: 2px 0;
+}
+
+.pt-dlg-button {
+ border: 1px solid #6a5acd;
+ font-size: 10px;
+ background-color: #ffffe0;
+}
+
+.pt-dlg-button:focus {
+ background-color: #ffffe0;
+}
+
+.pt-abbr {
+ cursor: help !important;
+ color: #000 !important;
+ text-decoration: none !important;
+ border-bottom: 1px dotted #333;
+}
+
+.pt-acronym {
+ cursor: help !important;
+ color: #000 !important;
+ text-decoration: none !important;
+ border-bottom: 1px dotted #333;
+}
+
+/* ButtonBar styles */
+#pt-btnBar, #pt-specialCharBar {
+ background-color: #fff;
+ border: 1px solid silver;
+ position: absolute;
+ margin:0;
+ padding:0;
+}
+
+#pt-btnBar ul, #pt-specialCharBar ul {
+ margin: 0;
+ padding: 0;
+}
+
+li.pt-btn, li.pt-char {
+ background-color: #fff;
+ display: block;
+ float: left;
+ list-style: none;
+ margin: 0;
+ margin: 0px 1px 0 1px;
+ padding: 0;
+ border: none;
+}
+
+li.pt-btn:first-child, li.pt-char:first-child {
+ margin-left: 0px;
+}
+
+a.pt-btnLink, a.pt-charLink {
+ border: 2px solid #fff;
+ display: block;
+ line-height:16px;
+ height: 16px;
+ width: 20px;
+ margin: 0;
+ padding: 0;
+}
+
+a.pt-btnLink:hover, a.pt-btn-pressed, a.pt-charLink:hover {
+ border: 2px solid silver;
+ margin: 0;
+ padding:0;
+ background: transparent;
+}
+
+#pt-btn-bold {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll 0px;
+}
+#pt-btn-italic {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -20px;
+}
+#pt-btn-underline {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -40px;
+}
+#pt-btn-strikethrough {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -60px;
+}
+#pt-btn-subscript {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -80px;
+}
+#pt-btn-superscript {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -100px;
+}
+
+#pt-btn-align_left {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -120px;
+}
+#pt-btn-align_center {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -140px;
+}
+#pt-btn-align_right {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -160px;
+}
+#pt-btn-justify {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -180px;
+}
+
+#pt-btn-indent {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -200px;
+}
+#pt-btn-outdent {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -220px;
+}
+
+#pt-btn-add_html {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -240px;
+}
+#pt-btn-delete_html {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -260px;
+}
+#pt-btn-cut {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -280px;
+}
+#pt-btn-copy {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -300px;
+}
+#pt-btn-paste {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -320px;
+}
+#pt-btn-undo {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -340px;
+}
+#pt-btn-redo {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -360px;
+}
+#pt-btn-specialchars {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -380px;
+}
+#pt-btn-help {
+ background: transparent url("/poortext/images/button_sprite.png") no-repeat scroll -400px;
+}
+
+
+/* Special Char Images */
+#pt-char-ldquo {
+ background: transparent url("/poortext/images/special_char_sprite.png") no-repeat;
+}
+#pt-char-rdquo {
+ background: transparent url("/poortext/images/special_char_sprite.png") no-repeat scroll -20px;
+}
+#pt-char-lsquo {
+ background: transparent url("/poortext/images/special_char_sprite.png") no-repeat scroll -40px;
+}
+#pt-char-rsquo {
+ background: transparent url("/poortext/images/special_char_sprite.png") no-repeat scroll -60px;
+}
+#pt-char-ndash {
+ background: transparent url("/poortext/images/special_char_sprite.png") no-repeat scroll -80px;
+}
Added: trunk/krang/htdocs/poortext/poortext.js
===================================================================
--- trunk/krang/htdocs/poortext/poortext.js (rev 0)
+++ trunk/krang/htdocs/poortext/poortext.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,2116 @@
+var PoorText;
+
+/** @fileoverview
+ Core functionality for PoorText elements
+*/
+
+/** PoorText provides basic markup and link insertion capabilities to HTMLDivElements.
+ @class
+ PoorText turns DIV elements in text input fields or textarea
+ elements of a special kind, allowing for what CMS systems really need:
+ basic markup plus link insertion plus phrase tags.
+ @constructor
+ @requires prototype-1.6.0.js -- the wellknown Ajax library, as well as Scriptaculous' effects.js and dragdrop.js
+ @param {STRING|HTMLDivElement} element The HTMLDivElement to turn into a PoorText element.
+ @param {Object} config An element-specific configuration object
+ overriding the global config in {@link PoorText#config}
+ @return PoorText
+*/
+PoorText = function (element, config) {
+ /**
+ The DIV element we want to turn into a PoorText element.
+ @type Node
+ @private
+ */
+ this.srcElement = '';
+
+ /**
+ The ID of the DIV element we want to turn into a PoorText element.
+ @type String
+ @private
+ */
+ this.id = '';
+
+ // Record element and id
+ this.srcElement = $(element);
+ this.id = this.srcElement.id;
+
+ /**
+ Flag to indicate whether the element has focus
+ @type Boolean
+ @private
+ */
+ this.focused = false;
+
+
+ /**
+ The node whose innerHTML will be edited.
+ @type Node
+ @private
+ */
+ this.editNode = null;
+
+ /**
+ The hidden form field used to return content. It will be created
+ on the fly. It's ID will be this.id+'_return'
+ @type Node
+ @private
+ */
+ this.returnHTML = null;
+
+ /**
+ The hidden form field used to return the indent. It will be
+ created on the fly. It's ID will be this.id+'_indent'.
+ @type Node
+ @private
+ */
+ this.returnIndent = null;
+
+ /**
+ The hidden form field used to return the text alignment. It
+ will be created on the fly. It's ID will be this.id+'_align'
+ @type Node
+ @private
+ */
+ this.returnAlign = null;
+
+ /**
+ The window object where editing takes place. When using iframes
+ this object differs from the main window.
+ @type Window
+ @private
+ */
+ this.window = null;
+
+ /**
+ The document object where editing takes place. When using iframes
+ this object differs from the global document object.
+ @type HTMLDocument
+ @private
+ */
+ this.document = null;
+
+ /**
+ The node receiving 'body styling'. When using iframes some
+ styles are effectively the styles applied to the body of the
+ iframe's document. These styles are enumerated in the class
+ array {@link PoorText#bodyStyles}.
+ @type Node
+ @private
+ */
+ this.styleNode = null;
+
+ /**
+ The node receiving 'frame styling'. When using iframes some
+ styles are indeed the styles applied to the iframe itsself.
+ These styles are enumerated in the class array {@link
+ PoorText#iframeStyles}
+ @type Node
+ @private
+ */
+ this.frameNode = null;
+
+ /**
+ The node receiving editing events.
+ @type Node
+ @private
+ */
+ this.eventNode = null;
+
+ /**
+ The browser-specific selection object updated on event
+ keyup. Not used when using an iframe (Firefox).
+ @type Range
+ @private
+ */
+ this.selection = null;
+
+ /**
+ The selection existing before selecting all via {@link
+ toggleSelectAll()} and restored afterwards.
+ @type Range
+ @private
+ */
+ this.selectedAllSelection = null;
+
+ /**
+ Flag indicating whether the content of editNode has been selected via this.selectall();
+ @type Boolean
+ @private
+ */
+ this.selectedAll = false;
+
+ /**
+ Object holding the selected link element (key name: elm) and
+ the selected range (key name: range) when modifying the
+ attributes of an existing link, abbreviation or acronym.
+ */
+ this.selected = {};
+
+ /**
+ Hash of key event handlers for ctrl_[alt_][shift_]x key events triggering editing commands.
+ @type Object
+ @private
+ */
+ this.keyHandlerFor = {};
+
+ /**
+ Array of event handlers registered on this elements eventNode.
+ @type Array
+ @private
+ */
+ this.eventHandlers = new Object();
+
+ /**
+ Flag to remember whether the specialCharBar (scb) is/was/should again be visible or not
+ @type Boolean
+ @private
+ */
+ this.scbVisible = false;
+
+ /**
+ Instance method to initialize PoorText elements.
+ @param {Object} config
+ @private
+ */
+ this.initialize(config);
+}
+
+/**
+ Default css classname of PoorText elements (used by {@link
+ PoorText#generateAll} and auto-generation). All DIV elements
+ having this CSS classname will be turned into PoorText elements if
+ {@link PoorText#autoload} is true.
+ This class name is 'poortext'.
+ @type String
+ @final
+*/
+PoorText.cssClass = 'poortext';
+
+/**
+ If true, turn all DIVs having a CSS classname of {@link PoorText#cssClass}
+ into PoorText elements -- via {@link PoorText#generateAll}.
+ @type Class bool
+*/
+PoorText.autoload = true;
+
+/**
+ Array of all created PoorText objects.
+ @type Class Array
+
+*/
+PoorText.objects = new Array();
+
+/**
+ Mapping {@link #id}s to corresponding PoorText objects.
+ @type Class Object
+*/
+PoorText.id2obj = new Object();
+
+/**
+ Pointer to object of currently focused PoorText element.
+ @type PoorText
+*/
+PoorText.focusedObj = null;
+
+/**
+ Name map of event handlers installed on editable HTMLDivElements.
+ Maps 'focus' to 'onFocus' etc.
+ The absence of an onBlur handler is on purpose: The class method
+ {@link PoorText#onBlur} takes care or it.
+ @type Object
+ @final
+ @private
+*/
+PoorText.events =
+ {
+ focus : 'onFocus',
+ keydown : 'onKeyDown',
+ keyup : 'onKeyUp',
+ click : 'onKeyUp',
+ dblclick : 'onKeyUp'
+ };
+
+/**
+ Stringify method for PoorText object.
+ @return the name of this class
+ @type String
+ @private
+ @final
+*/
+PoorText.toString = function() { return 'PoorText' }
+
+/**
+ Filter correcting some html messup.
+ 1. compress <i>s</i><i>ix</i> to <i>six</i>
+ 2. chop empty markup tags
+ @param {Node} node The node whose innerHTML will we corrected
+ @return the node with its innerHTML corrected
+ @type Node
+ @private
+*/
+PoorText.correctMarkup = function(node) {
+ var html = node.innerHTML;
+ html = html.replace(/<\/([^>]+)><\1>/g , "" ) // compress <i>s</i><i>ix</i> to <i>six</i>
+ .replace(/<([^<]+)>\s+<\/\1>/g, " " ) // chop whitespace only tags and collapse white space
+ .replace(/<([^<]+)><\/\1>/g , "" ) // chop empty tags
+
+ // collapse whitespace
+ .replace(/(?: )+/g, ' ')
+ .replace(/\u00A0+/g, ' ') // this is in unicode: WebKit prefers this
+ .replace(/\s+/g, ' ')
+
+ // remove <br> at end, while taking care of trailing closing tags and possible whitespace
+ .replace(/(?:<br>\s*)+\s*((?:<\/[^>]+>)*)\s*$/, '$1')
+
+ // trim leading and trailing whitespace
+ .replace(/^\s+|\s+$/g, '')
+
+ // trim DIV tags
+ .replace(/(?:<\/?div>)+/g, '');
+
+ node.innerHTML = html;
+ return node;
+};
+
+/**
+ Filter adding our '_poortext_url' attribut to links. We don't
+ want browsers to mess up the HREF attribute of links entered by
+ the user. So we remember the URL entered in a previous editing
+ session and now coming from the server, and we remember it with a
+ custom attribut named '_poortext_url'. When sending the edited
+ content back to the server or extracting it via {@link #getHtml},
+ we'll delete '_poortext_url' after writing its content back to the
+ href attribute of the link (See {@link PoorText#removeUrlProtection}).
+ The same procedure is applied to ABBR and ACRONYM tags.
+ @param {Node} node The node whose <a> tags will be treated
+ @return the node with URL protection added.
+ @type Node
+ @private
+*/
+PoorText.addUrlProtection = function(node) {
+ // Remember URL in _poortext_url attribute
+ var links = node.getElementsByTagName('A');
+ $A(links).each(function(link) {
+ link = $(link);
+ if (link.hasAttribute('href')) {
+ link.setAttribute('_poortext_url', PoorText.getHref(link));
+ link.setAttribute('_poortext_tag', 'a');
+ PoorText.setClass(link, 'pt-a');
+ }
+ });
+
+ // Substitute phrase markup with custom link elements
+ var isIE6 = false;
+ ['abbr', 'acronym'].each(function(tag) {
+ var elements = node.getElementsByTagName(tag);
+ $A(elements).each(function(elm) {
+ // IE6 does not support the ABBR tag: fix it
+ if (!elm.innerHTML) {
+ isIE6 = true;
+ return;
+ }
+ var link = $(document.createElement('a'));
+ link.setAttribute('title', elm.getAttribute('title'));
+ link.setAttribute('_poortext_tag', tag);
+ link.setAttribute('href', '');
+ PoorText.setClass(link, 'pt-'+tag);
+ link.innerHTML = elm.innerHTML;
+ elm.parentNode.replaceChild(link, elm);
+ })});
+
+ if (isIE6) PoorText.__abbrFixIE6(node);
+
+ return node;
+};
+
+/**
+ Filter removing our '_poortext_url' attribut from links after
+ writing its contents back to their HREF attribut. This is the
+ counterpart of {@link PoorText#removeUrlProtection}), which comes with
+ more detailed information.
+ The same procedure is applied to ABBR and ACRONYM tags.
+ @param {Node} node The node whose <a> tags will be treated.
+ @return The node with URL protection removed
+ @type Node
+ @private
+*/
+PoorText.removeUrlProtection = function(node) {
+ var links = node.getElementsByTagName('A');
+ $A(links).each(function(link) {
+ link = $(link);
+ if (link.hasAttribute('_poortext_url')) {
+ link.setAttribute('href', link.getAttribute('_poortext_url'));
+ link.removeAttribute('_poortext_url');
+ link.removeAttribute('_poortext_tag');
+ link.removeAttribute('class');
+ link.removeAttribute('className');
+ link.removeAttribute('_counted'); // IE stuff
+ }
+ else { // fake links to be turned into phrase markup
+ var tagName = link.getAttribute('_poortext_tag');
+ var tag = document.createElement(tagName);
+ tag.setAttribute('title', link.getAttribute('title'));
+ tag.innerHTML = link.innerHTML;
+ link.parentNode.replaceChild(tag, link);
+ }
+ });
+ return node;
+};
+
+/**
+ Array of filter functions to be applied at creation time. The
+ {@link #srcElement} innerHTML is passed through these filters, the
+ output being stored in {@link #returnHTML}.
+ @type Class Array
+ @private
+*/
+PoorText.returnFilters = [];
+
+/**
+ Array of filter functions for incoming HTML. These filters are
+ applied when setting {@link #editNode} with the contents of {@link
+ #srcElement}. This typically occurs in {@link #makeEditable}.
+ @type Class Array
+ @private
+*/
+PoorText.inFilters = [ PoorText.addUrlProtection ];
+
+/**
+ Array of filter function for outgoing HTML. These filters will be
+ applied when setting {@link #returnHTML} with the contents of
+ {@link #editNode}. This typically occurs {@link PoorText#onBlur}.
+ @type Class Array
+ @private
+*/
+PoorText.outFilters = [ PoorText.correctMarkup,
+ PoorText.removeUrlProtection ];
+
+/**
+ Global configuration API for PoorText users.<br> The following
+ options may also be specified on a per-instance basis. In this case
+ they override the global configuration provided in PoorText.config{}.
+
+ <b>form</b> {STRING id | HTMLFormElement} - The HTML form this
+ PoorText element belongs to. May be specified as ID or
+ HTMLFormElement. If not specified, it will be derived from the
+ DIV itsself by searching the document tree upwards for a parent
+ FORM element. If the DIV does not reside in any FORM and the
+ form option is not specified, an error of type OutsideFormError
+ will be thrown.<br/>
+
+ <b>type</b> {STRING text|textarea} - The type of the PoorText
+ element. The type of a PoorText element maybe 'text' or
+ 'textarea'. Currently, the only difference between the two
+ flavours is that 'text'-type elements, like regular text input
+ fields, do nothing but alert the user when she presses 'RET'.
+ The type of an element may be specified<br>(a) using the
+ present global config option<br>(b) via a non-standard HTML
+ 'type' attribute set on the DIV we want to make editable<br>(c)
+ the instance config option 'type' -- precedence in this order.
+ The default is 'text'.<br/>
+
+ <b>cssClass</b> {STRING} - The CSS class of DIV elements to be
+ turned into PoorText elements. PoorText looks for DIVs having
+ this CSS class elements when {@link #autoload} is true
+ or when calling {@link PoorText#generateAllWithCssClass}. The
+ default is {@link PoorText#cssClass}.<br/>
+
+ <b>deferIframeCreation</b> {BOOL} - If true, which is the default,
+ the editable iframe will only be created onMouseOver. This
+ speeds up loading for pages having a lot of PoorText elements.<br/>
+
+ <b>iframeHead</b> {STRING} - A string to insert into the HEAD
+ section of the editable iframes created by PoorText. Defaults
+ to the empty string.
+
+ <b>onFocus</b> {FUNCTION} - Function to be called when focussing a
+ PoorText element. The default sets the PoorText element's
+ border-style to 'inset'.<br/>
+
+ <b>onBlur</b> {FUNCTION} - Function to be called when bluring a
+ PoorText element. The default sets the PoorText element's
+ border-style to 'solid'.<br/>
+
+ <b>onSubmit</b> {FUNCTION} - Function to be called when the form is
+ submitted. No default action.<br/>
+
+ <b>availableCommands</b> {Array} - List of command names available
+ on PoorText elements. The default is<br>
+ 'toggle_selectall', 'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript',<br/>
+ 'cut', 'copy', 'paste',<br/>
+ 'align_left', 'align_center', 'align_right', 'justify',<br/>
+ 'indent', 'outdent',<br/>
+ 'add_html', 'delete_html',<br/>
+ 'redo', 'undo',<br/>
+ 'specialchars' and 'help'.<br/>
+ You can't specify more commands, but you can restrict the list.<br/>
+
+ <b>specialChars</b> {OBJECT} - object mapping charnames to unicode
+ codepoint strings. These chars appear in the special char bar
+ if attachSpecialCharBar is true (see below). And you may
+ provide shortcuts for them, mapping their names to shortcuts
+ using the config option 'shortcutFor'.<br/>
+
+ <b>shortcutFor</b> {OBJECT} - object mapping command names (see
+ 'availableCommands') and charnames (see 'specialChars') to shortcuts.<br/>
+ The default is:<br>
+ bold : 'ctrl_b',<br/>
+ italic : 'ctrl_i',<br/>
+ underline : 'ctrl_u',<br/>
+ subscript : 'ctrl_d',<br/>
+ superscript : 'ctrl_s',<br/>
+ strikethrough : 'ctrl_t',<br/>
+ toggle_selectall : 'ctrl_a',<br/>
+ add_html : 'ctrl_l',<br/>
+ delete_html : 'ctrl_shift_l',<br/>
+ redo : 'ctrl_y',<br/>
+ undo : 'ctrl_z',<br/>
+ help : 'ctrl_h',<br/>
+ esc : 'escape',<br/>
+ enter : 'enter',<br/>
+ cut : 'ctrl_x',<br/>
+ copy : 'ctrl_c',<br/>
+ paste : 'ctrl_v',<br/>
+ specialchars : 'ctrl_6',<br/>
+ align_left : 'ctrl_q',
+ align_center : 'ctrl_e',
+ align_right : 'ctrl_r',
+ justify : 'ctrl_w',
+ indent : 'tab',
+ outdent : 'shift_tab',
+ lsquo : 'ctrl_4',<br/>
+ rsquo : 'ctrl_5',<br/>
+ ldquo : 'ctrl_2',<br/>
+ rdquo : 'ctrl_3',<br/>
+ ndash : 'ctrl_0',<br/>
+
+ <b>attachButtonBar</b> {BOOL} - If true, attach the button bar on top
+ of the focused PoorText element. If false, the availableCommands
+ are only available via their shortcuts.<br/>
+
+ <b>attachSpecialCharBar</b> {BOOL} - If true, attach the
+ specialChar bar on top of the button bar. If false, the char bar
+ may be displayed via the specialchar bar button (the omega sign).<br/>
+
+ <b>lang</b> {STRING} - A RFC3066-style language tag to localize
+ PoorText strings. Lexicons reside in lang/. Defaults to English.<br/>
+
+ <b>indentSize</b> {NUMBER} - The number of pixel the text content
+ will be shifted to the right when pressing KEY_TAB. Defaults to
+ 20px.<br/>
+
+ @type Class Object
+*/
+PoorText.config = {};
+
+PoorText.prototype = {
+ initialize : function (config) {
+ this.setConfig(config);
+ this.insertReturnElements();
+ this.makeEditable();
+ this.attachKeymap();
+ },
+
+ functionFor : {
+ /**@ignore*/ bold : function() {this.markup('bold' )},
+ /**@ignore*/ italic : function() {this.markup('italic' )},
+ /**@ignore*/ underline : function() {this.markup('underline' )},
+ /**@ignore*/ subscript : function() {this.markup('subscript' )},
+ /**@ignore*/ superscript : function() {this.markup('superscript' )},
+ /**@ignore*/ strikethrough : function() {this.markup('strikethrough')},
+ /**@ignore*/ toggle_selectall : function() {this.toggleSelectAll() },
+ /**@ignore*/ add_html : function() {this.addHTML() },
+ /**@ignore*/ delete_html : function() {this.deleteHTML() },
+ /**@ignore*/ redo : function() {this.markup('redo') },
+ /**@ignore*/ undo : function() {this.undo() },
+ /**@ignore*/ help : function() {this.showHelp() },
+ /**@ignore*/ esc : function() {window.focus()},
+ /**@ignore*/ cut : function() {this.markup('cut' )},
+ /**@ignore*/ copy : function() {this.markup('copy' )},
+ /**@ignore*/ paste : function() {this.markup('paste' )},
+ /**@ignore*/ specialchars : function() {this.toggleSpecialCharBar() },
+ /**@ignore*/ align_left : function() {this.setTextAlign('left' )},
+ /**@ignore*/ align_center : function() {this.setTextAlign('center' )},
+ /**@ignore*/ align_right : function() {this.setTextAlign('right' )},
+ /**@ignore*/ justify : function() {this.setTextAlign('justify')},
+ /**@ignore*/ indent : function() {this.setTextIndent() },
+ /**@ignore*/ outdent : function() {this.setTextOutdent() }
+ },
+
+ /**
+ Configure this PoorText instance: Use the builtin defaults,
+ overwrite them with the user-provided global configuration in
+ {@link PoorText#config}, then overwrite them with
+ instance-specific configuration.
+ @param {Object} config
+ @return nothing
+ @private
+ */
+ setConfig : function (config) {
+ config = (config || {});
+
+ // Defaults
+ this.shortcutFor = {
+ bold : 'ctrl_b',
+ italic : 'ctrl_i',
+ underline : 'ctrl_u',
+ subscript : 'ctrl_d',
+ superscript : 'ctrl_s',
+ strikethrough : 'ctrl_t',
+ toggle_selectall : 'ctrl_a',
+ add_html : 'ctrl_l',
+ delete_html : 'ctrl_shift_l',
+ redo : 'ctrl_y',
+ undo : 'ctrl_z',
+ help : 'ctrl_h',
+ esc : 'escape',
+ enter : 'enter',
+ cut : 'ctrl_x',
+ copy : 'ctrl_c',
+ paste : 'ctrl_v',
+ specialchars : 'ctrl_6',
+ align_left : 'ctrl_q',
+ align_center : 'ctrl_e',
+ align_right : 'ctrl_r',
+ justify : 'ctrl_w',
+ indent : 'tab',
+ outdent : 'shift_tab',
+ lsquo : 'ctrl_4',
+ rsquo : 'ctrl_5',
+ ldquo : 'ctrl_2',
+ rdquo : 'ctrl_3',
+ ndash : 'ctrl_0'
+ };
+
+ this.config = {
+ type : 'text',
+ deferIframeCreation : true, // if true, create iframe when srcElement.mouseover()
+ iframeHead : '', // string to insert in the HEAD section of Gecko's iframe
+ onFocus : function() {this.setStyle({borderStyle:'inset'})},
+ onBlur : function() {this.setStyle({borderStyle:'solid'})},
+ availableCommands : $A([
+ 'toggle_selectall', 'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript',
+ 'cut', 'copy', 'paste',
+ 'align_left', 'align_center', 'align_right', 'justify',
+ 'indent', 'outdent',
+ 'add_html', 'delete_html',
+ 'redo', 'undo', 'specialchars', 'help'
+ ]),
+ specialChars : $H({
+ ldquo : "\u201C",
+ rdquo : "\u201D",
+ lsquo : "\u2018",
+ rsquo : "\u2019",
+ ndash : "\u2013"
+ }),
+ attachButtonBar : false,
+ attachSpecialCharBar : false,
+ lang : 'de',
+ indentSize : 20
+ };
+
+ // Merge in global config shortcuts
+ Object.extend(this.shortcutFor, PoorText.config.shortcutFor);
+
+ // Merge in instance config shortcuts
+ Object.extend(this.shortcutFor, config.shortcutFor);
+
+ // Merge in global config
+ Object.extend(this.config, PoorText.config);
+
+ // Set HTML type attrib between global config merge and instance config merge!!
+ var type = this.srcElement.getAttribute('type');
+ if (type) this.config.type = type;
+
+ // Merge in instance config
+ Object.extend(this.config, config);
+
+ // TAB and ESC commands are always available
+ this.config.availableCommands.push('tab', 'esc');
+
+ // Record the form
+ var form = this.config.form;
+ this.form = form ? $(form) : this.srcElement.up('form');
+ if (! this.form) {
+ var e = new Error();
+ e.message = 'No FORM was specified on instance creation and instance "'
+ + this.id
+ + '" is not a child of any FORM tag';
+ e.name = "OutsideFormError";
+ throw e;
+ }
+
+ // For 'text' fields, attach empty function on 'enter' keypress
+ if (this.config.type == 'text') {
+ this.config.availableCommands.push('enter');
+ }
+
+ // Call the element's onSubmit method when submitting the form
+ Event.observe(this.form, 'submit', this.onSubmit.bindAsEventListener(this), true);
+
+ // Some bookkeeping
+ PoorText.objects.push(this);
+ PoorText.id2obj[this.id] = this;
+ },
+
+ /**
+ Instance method: Return true if the given command is enabled in
+ PoorText. This is just a wrapper around
+ document.queryCommandEnabled()
+ @param {STRING} command
+ @return boolean
+ @private
+ */
+ queryCommandEnabled : function(cmd) {
+ return PoorText.hasCommand[cmd];
+ },
+
+ /**
+ Instance method: Return true if the given command is active,
+ e.g. the cursor is on 'bold' text. This is just a wrapper
+ around document.queryCommandState()
+ @param {STRING} command
+ @return boolean
+ @private
+ */
+ queryCommandState : function(cmd) {
+ if (cmd == 'selectall') return this.selectedAll;
+ return this.document.queryCommandState(cmd);
+ },
+
+ /**
+ Instance method to attach the configured shortcuts to their
+ functions.
+ @param none
+ @return nothing
+ @private
+ */
+ attachKeymap : function () {
+ // keymap for commands
+ this.filterAvailableCommands().each(function(cmd) {
+ if (this.config.attachSpecialCharBar && cmd == 'specialchars') return;
+ this.keyHandlerFor[this.shortcutFor[cmd]] = this.functionFor[cmd];
+ }.bind(this));
+
+ // ENTER key handler depends on type
+ if (this.config.type == 'text') {
+ this.keyHandlerFor['enter'] = function() {
+ this.notify("The 'ENTER' key you pressed is not allowed in this context!");
+ }
+ } else {
+ if (!Prototype.Browser.Gecko) {
+ PoorText.__enterKeyHandler(this);
+ }
+ }
+
+ // keymap for special chars
+ this.config.specialChars.each(function(special) {
+ this.keyHandlerFor[this.shortcutFor[special.key]]
+ = function () {this.insertHTML(special.value, false)};
+ }.bind(this));
+ },
+
+ /**
+ Handle keyDown events for PoorText elements.
+ @param {EVENT} event
+ @return nothing
+ @private
+ */
+ onKeyDown : function(event) {
+ return this.dispatchKey(event);
+ },
+
+ // Borrowed from David Flanagan
+ /**@ignore*/
+ dispatchKey : function(e) {
+
+ // We start off with no modifiers and no key name
+ var modifiers = ""
+ var keyname = null;
+
+ // Gecko || MSIE
+ var code = (e.which || e.keyCode);
+
+ // Ignore keydown events for Shift, Ctrl, and Alt
+ if (code == 16 || code == 17 || code == 18) return;
+
+ // Get the key name from our mapping
+ keyname = PoorText.keyCodeToFunctionKey[code];
+
+ // If this wasn't a function key, but the ctrl or alt modifiers are
+ // down, we want to treat it like a function key
+ if (!keyname && (e.altKey || e.ctrlKey))
+ keyname = PoorText.keyCodeToPrintableChar[code];
+
+ // If we found a name for this key, figure out its modifiers.
+ // Otherwise just return and ignore this keydown event.
+ if (keyname) {
+ if (e.altKey) modifiers += "alt_";
+ if (e.ctrlKey) modifiers += "ctrl_";
+ if (e.shiftKey) modifiers += "shift_";
+ }
+ else return;
+
+ // Now that we've determined the modifiers and key name, we look for
+ // a handler function for the key and modifier combination
+ var func = this.keyHandlerFor[modifiers+keyname];
+
+ if (func) { // If there is a handler for this key, handle it
+ func.call(this, e);
+ Event.stop(e);
+ }
+
+ // Gecko needs special things
+ if (Prototype.Browser.Gecko) {
+ this.__afterKeyDispatch(modifiers+keyname);
+ }
+ },
+
+ /**
+ Handle keyUp events for PoorText elements.
+ @param {EVENT} event
+ @return nothing
+ @private
+ */
+ onKeyUp : function(event) {
+
+ this.updateButtonBar(event);
+
+ this.afterOnKeyUp(event);
+
+ if (event) Event.stop(event);
+ },
+
+ /**
+ Instance method to update the button bar (called by the onKeyUp
+ handler)
+ @param {Event} event
+ @return nothing
+ @private
+ */
+ updateButtonBar : function(event) {
+ // no button bar, no update
+ if (!$('pt-btnBar')) return;
+
+ // On IE, markup via button automagically marks up the whole
+ // word the cursor is on. But the button bar update process
+ // needs to be deferred a bit in this special case. If the
+ // word is selected no deferring is necessary. Gecko ignores
+ // this magic.
+ setTimeout(function() {
+ // make sure we are only called once
+ if (this.keyUpHandlerIsExecuting) return;
+ this.keyUpHandlerIsExecuting = true;
+
+ // remove 'btn-pressed' CSS class from all buttons
+ $$('a.pt-btnLink').invoke('removeClassName', 'pt-btn-pressed');
+
+ // add a 'btn-pressed' CSS class for active commands
+ PoorText.markupButtons.each(function(tag) {
+ if (this.queryCommandState(tag)) {
+ $('pt-btn-'+tag).firstDescendant().addClassName('pt-btn-pressed');
+ }
+ }.bind(this));
+ }.bind(this), 1);
+
+ // reset control
+ this.keyUpHandlerIsExecuting = false;
+ },
+
+ /**
+ Instance method to toggle the display of the special-chars bar.
+ @param {EVENT} event
+ @returns nothing
+ @private
+ */
+ toggleSpecialCharBar : function(event) {
+ // toggle flag
+ this.scbVisible = !this.scbVisible;
+
+ // act upon flag state
+ this.showHideSpecialCharBar();
+ },
+
+ /**
+ Instance method to show/hide the special-chars bar depending on
+ an internal status flag.
+ @param none
+ @returns nothing
+ @private
+ */
+ showHideSpecialCharBar : function() {
+ if (this.scbVisible) {
+ // show special chars and replace button img
+ PoorText.specialCharBar.attach(this);
+ $('pt-btn-specialchars')
+ .title = PoorText.cmdToDisplayName('hide_specialchars');
+ } else {
+ // hide special chars and replace button img
+ $('pt-specialCharBar').hide();
+ $('pt-btn-specialchars')
+ .title = PoorText.cmdToDisplayName('show_specialchars');
+ }
+ this.afterShowHideSpecialCharBar();
+ },
+
+ /**
+ Instance method called when a PoorText field receives focus.
+ @param {EVENT} event
+ @returns nothing
+ @private
+ */
+ onFocus : function(event) {
+
+ // maybe hide markup dialog
+ if ($('pt-popup-addHTML')) $('pt-popup-addHTML').hide();
+
+ // short-circuit
+ if (this.focused) return;
+
+ // pseudo onblur handler for previously focused object
+ PoorText.onBlur();
+
+ // execute the configured onFocus handler
+ if (this.config.onFocus && typeof(this.config.onFocus) == 'function') {
+ this.config.onFocus.call(this);
+ }
+
+ // remember me
+ this.focused = true;
+ PoorText.focusedObj = this;
+
+ // maybe attach the button bar
+ if (this.config.attachButtonBar) {
+ PoorText.buttonBar.attach(this);
+ }
+
+ // maybe attach specialChar bar
+ if (this.config.attachSpecialCharBar || this.scbVisible) {
+ PoorText.specialCharBar.attach(this);
+ }
+
+ // update Help popup if it's visible
+ if (PoorText.Popup.help && PoorText.Popup.help.visible()) {
+ this.showHelp(true);
+ }
+ },
+
+ /**
+ onSubmit handler. Default does nothing.
+ @param {EVENT} event
+ @return nothing
+ */
+ onSubmit : function(event) {
+ },
+
+ /**
+ Instance method to attach a named event handler to the current
+ element.
+ @param {STRING} - the name of the event (keydown, click etc.)
+ @param {STRING} - an arbitrary string identifying this event handler
+ @param {FUNCTION} - the event handler function
+ @param {BOOL} - the useCapture flag
+ */
+ observe : function(type, name, handler, useCapture) {
+ var func = handler.bindAsEventListener(this);
+ Event.observe(this.eventNode, type, func, useCapture);
+ if (!this.eventHandlers[type]) this.eventHandlers[type] = new Object();
+ this.eventHandlers[type][name] = [func, useCapture];
+ },
+
+ /**
+ Instance method to detach a named event handler from the
+ current element
+ @param {STRING} - the name of the event (keydown, click etc.)
+ @param {STRING} - the identifier under which the event handler
+ has been registered using {@link .observe()}
+ @returns nothing
+ @private
+ */
+ stopObserving : function(type, name) {
+ try {
+ var handlerSpec = this.eventHandlers[type][name];
+ var func = handlerSpec[0];
+ var useCapture = handlerSpec[1];
+ Event.stopObserving(this.eventNode, type, func, useCapture);
+ delete this.eventHandlers[type][name];
+ } catch (e) {}
+ },
+
+ /**
+ Instance method to remove all event handlers installed via
+ {@link .observe()}
+ @param none
+ @returns nothing
+ @private
+ */
+ removeAllEventHandlers : function () {
+ if (!this.eventHandlers) return;
+ for (type in this.eventHandlers) {
+ for (name in this.eventHandlers[type]) {
+ this.stopObserving(type, name);
+ }
+ }
+ this.eventHandlers = null;
+ },
+
+ /**
+ Instance method to get the HTML out of the PoorText
+ element. May be used in onSubmit handlers.
+ @param none
+ @return the HTML typed into PoorText elements
+ @type String
+ */
+ getHtml : function() {
+ return this.applyFiltersTo(this.editNode.cloneNode(true), PoorText.outFilters);
+ },
+
+ /**
+ Instance method to set the innerHTML of {@link PoorText#editNode}.
+ @param {Node} node the node whose HTML should be set as the innerHTML of editNode
+ @param {Array} filters an array of filters applied to node before setting the editNode's innerHTML
+ @return nothing
+ */
+ setHtml : function(node, filters) {
+ this.editNode.innerHTML = this.applyFiltersTo(node, filters).innerHTML;
+ },
+
+ /**@ignore*/
+ applyFiltersTo : function(node, filters) {
+ filters.each(function(filter) {
+ node = filter(node);
+ });
+ return node;
+ },
+
+ /**@ignore*/
+ notify : function(a) { alert(PoorText.L10N.localize(a)) },
+
+ /**@ignore*/
+ addHTML : function() {
+
+ // Get existing link
+ var sel = this.selected = this.getLink();
+
+ // No link && no selection
+ if (sel.msg == 'showAlert') {
+ this.notify("You have to select some text to insert a HTML element.");
+ return;
+ }
+
+ var oldElm = sel.elm;
+
+ if (oldElm) {
+ if (oldElm.getAttribute('_poortext_tag') == 'link') {
+ var mysavedurl = oldElm.getAttribute('_poortext_url');
+ oldElm.url = mysavedurl ? mysavedurl : PoorText.getHref(oldElm);
+ }
+ }
+
+
+ // Create popup and hook in the dialog html
+ window.focus();
+
+ var which = 'addHTML';
+
+ var popup = PoorText.Popup.get(which);
+ popup.innerHTML = PoorText.L10N.localizeDialog(PoorText.htmlFor[which]);
+
+ var dlgForm = $('pt-dlg-form-'+which);
+ var urlField = dlgForm['pt-dlg-url'];
+ var titleField = dlgForm['pt-dlg-tooltip'];
+
+ if (oldElm) {
+ // Set tag
+ var tag = $A(dlgForm['tag']).find(function(tag) {
+ return tag.id == 'pt-dlg-' + oldElm.getAttribute('_poortext_tag');
+ });
+ tag.checked = true;
+
+ // Set title
+ var title = oldElm.getAttribute('title');
+ if (title) titleField.value = title;
+
+ // Set URL
+ var url = oldElm.getAttribute('_poortext_url');
+ if (tag.id == 'pt-dlg-a') {
+ urlField.value = url;
+ $('pt-dlg-url-row').show();
+ setTimeout(function() {
+ urlField.focus();
+ urlField.select();
+ },50);
+ }
+ else {
+ urlField.value = 'http://';
+ $('pt-dlg-url-row').hide();
+ setTimeout(function() {
+ titleField.focus();
+ titleField.select();
+ },50);
+ }
+ }
+ else {
+ urlField.value = 'http://';
+ setTimeout(function() {
+ $('pt-dlg-a').focus();
+ }, 50);
+ }
+
+ // Finally, show the dialog
+ popup.show();
+ PoorText.Popup.positionIt(popup, which);
+ },
+
+ /**@ignore*/
+ deleteHTML : function() {
+ var sel = this.getLink();
+
+ if (sel.msg == 'showAlert') {
+ this.notify("You didn't select any HTML element to delete!");
+ return;
+ }
+
+ try { sel.elm.removeAttribute('class'); } catch(e) {}
+
+ try {
+ this.doDeleteHTML();
+ }
+ catch(error) {
+ alert("Couldn't delete the link because of "+error);
+ }
+ },
+
+ /**@ignore*/
+ insertReturnElements : function() {
+ // Hidden form element used to return the HTML
+ var id = this.id;
+ var rid = id + '_return';
+ var re = $(rid);
+ if (!re) {
+ // We don't have it: make it
+ var val = this.applyFiltersTo(this.srcElement, PoorText.returnFilters).innerHTML;
+ re = new Element('input', {type: 'hidden', id: rid, name: id, value: val});
+ this.form.appendChild(re);
+ }
+ this.returnHTML = re;
+
+ // Hidden form elements used to return the text INDENT and ALIGN
+ [['indent', this.getTextIndent()],
+ ['align' , this.getTextAlign() ]].each(function(el) {
+ var rid = id + '_' + el[0];
+ var re = $(rid);
+ if (!re) {
+ re = new Element('input', {type: 'hidden', id: rid, name: rid, value: el[1]});
+ this.form.appendChild(re);
+ }
+ this["return" + el[0].capitalize()] = re;
+ }.bind(this));
+
+ },
+
+ /**@ignore*/
+ showHelp : function(isVisible) {
+
+ if (!isVisible) window.focus();
+
+ var which = 'help';
+
+ // Build the help row html
+ var tmpl = PoorText.htmlFor[which].split('HERE');
+ var html = PoorText.L10N.localizeDialog(tmpl[0]);
+
+ // add shortcut help for buttons
+ this.config.availableCommands.each(function(cmd) {
+ if (/^(tab|esc|enter|help)/.test(cmd)) return;
+ if (this.config.attachSpecialCharBar && cmd == 'specialchars') return;
+ html += '<tr><td>';
+ html += PoorText.cmdToDisplayName(cmd)
+ html += '</td><td>';
+ html += PoorText.cmdToDisplayShortcut(this, cmd);
+ html += '</td></tr>';
+ }.bind(this));
+
+ // add shortcut help for specialChars
+ this.config.specialChars.each(function(sc) {
+ html += '<tr><td>';
+ html += '<img src="/poortext/images/'+sc.key+'.png">';
+ html += '</td><td>';
+ html += PoorText.cmdToDisplayShortcut(this, sc.key);
+ html += '</td></tr>';
+ }.bind(this));
+
+ // add finally add the Close button
+ html += '<tr><td></td><td style="text-align:right"><input type="button" value="' + PoorText.L10N.localize('Close')+'" id="pt-dlg-close" class="pt-dlg-button"></td></tr>';
+ html += tmpl[1];
+
+ // Insert the help html into the popup
+ var popup = PoorText.Popup.get(which);
+ popup.innerHTML = html;
+
+ // Show the popup and focus the 'Close' button
+ popup.show();
+ PoorText.Popup.positionIt(popup, which);
+ if (!isVisible) setTimeout(function() {$('pt-dlg-close').focus() }, 50 );
+ },
+
+ setTextAlign : function(align) {
+ this.setStyle({textAlign : align});
+ this.restoreSelection(); // WebKit needs this
+ },
+
+ getTextAlign : function() {
+ return this.getStyle('textAlign');
+ },
+
+ setTextIndent : function() {
+ var oldIndent = this.getTextIndent();
+ var oldWidth = parseInt(this.getStyle('width'));
+ var newIndent = oldIndent + this.config.indentSize + 'px';
+ var newWidth = oldWidth - (this.config.indentSize * 2) + 'px';
+ this.setStyle({paddingLeft : newIndent, paddingRight: newIndent, width: newWidth});
+ this.restoreSelection(); // WebKit needs this
+ },
+
+ setTextOutdent : function() {
+ var oldIndent = this.getTextIndent();
+ var oldWidth = parseInt(this.getStyle('width'));
+ var newIndent = oldIndent - this.config.indentSize;
+ var newWidth = oldWidth + (this.config.indentSize * 2) + 'px';
+ if (newIndent < 0) {
+ newIndent = 0;
+ newWidth = oldWidth;
+ }
+ newIndent += 'px';
+ this.setStyle({paddingLeft : newIndent, paddingRight: newIndent, width: newWidth});
+ this.restoreSelection(); // WebKit needs this
+ },
+
+ getTextIndent : function() {
+ return parseInt(this.getStyle('paddingLeft'));
+ },
+
+ storeForPostBack : function() {
+ try {
+ this.returnHTML.value = this.getHtml();
+ this.returnIndent.value = this.getTextIndent();
+ this.returnAlign.value = this.getTextAlign();
+ } catch(er) {}
+ }
+};
+
+
+/**
+ Class method to generate PoorText elements for all DIVs having a
+ CSS class of {@link PoorText#config.cssClass} or the default 'poortext'.
+ @param none
+ @return nothing
+ @private
+*/
+PoorText.generateAll = function() {
+ PoorText.generateAllWithCssClass(PoorText.config.cssClass || 'poortext');
+ PoorText.finish_init();
+};
+
+
+PoorText.finish_init = function() {
+ // Pseudo onBlur event for PT objects
+ Event.observe(document, 'click', PoorText.onBlur);
+
+ // Make sure no PT field has focus
+ window.blur(); window.focus();
+}
+
+/**
+ Class method to generate PoorText elements for all DIVs having the
+ given CSS class. The generated PoorText objects are accessible via
+ {@link PoorText#objects}, {@link PoorText#id2obj} and {@link
+ PoorText#focusedObj}.
+ @param {STRING} CSS class name
+ @return nothing
+*/
+PoorText.generateAllWithCssClass = function(cssClass) {
+ $$('.'+cssClass).each(function(pt) { new PoorText(pt) });
+};
+
+/**
+ Class method to generate PoorText elements for all configured DIVs
+ if {@link PoorText#autoload} is true.
+ @param none
+ @return nothing
+ @private
+*/
+var _timer;
+PoorText.onload = function () {
+ // quit if this function has already been called
+ if (arguments.callee.done) return;
+
+ // flag this function so we don't do the same thing twice
+ arguments.callee.done = true;
+
+ // kill the timer
+ if (_timer) {
+ clearInterval(_timer);
+ _timer = null;
+ }
+
+ // initialize all PoorText elements
+ if (PoorText.autoload == true) {
+ PoorText.generateAll();
+ } else {
+ PoorText.init();
+ PoorText.finish_init();
+ }
+};
+
+PoorText.init = function() {};
+
+/**
+ Class method, i.e. a pseudo onBlur handler. It can be triggered by
+ main window's click event or the focus event of PT objects. In the
+ latter case it is triggered by the onFocus event of the <b>next</b>
+ focused object, but acts on the <b>previously</b> focused object.
+ @param None
+ @return nothing
+ @private
+*/
+PoorText.onBlur = function(event) {
+ // Radiobuttons need the click event to bubble to the window, but
+ // under certain circumstances we don't want the window's onClick
+ // handler to be triggered
+ if (PoorText.cancelClickOnWindow) {
+ PoorText.cancelClickOnWindow = false;
+ return;
+ }
+
+ // hide the btn bar, the special char bar and the markup popup
+ if ($('pt-btnBar')) $('pt-btnBar').hide();
+ if ($('pt-specialCharBar')) $('pt-specialCharBar').hide();
+ if ($('pt-popup-addHTML')) $('pt-popup-addHTML').hide();
+ // onBlur stuff for previously focused PT field
+ if (PoorText.focusedObj) {
+ PoorText.focusedObj.selectionCollapseToEnd();
+ PoorText.focusedObj.storeForPostBack();
+ PoorText.focusedObj.config.onBlur.call(PoorText.focusedObj);
+ PoorText.focusedObj.focused = false;
+ PoorText.focusedObj = null;
+ }
+}.bindAsEventListener({});
+
+/**
+ Object telling which markup commands are supported by PoorText
+ @type Class Object command map
+ @final
+ @private
+*/
+PoorText.hasCommand = {
+ bold : 1,
+ copy : 1,
+ createlink : 1,
+ cut : 1,
+ inserthtml : 1,
+ italic : 1,
+ paste : 1,
+ strikethrough : 1,
+ subscript : 1,
+ superscript : 1,
+ underline : 1,
+ unlink : 1
+};
+
+/**
+ This object maps keydown keycode values to key names for printable
+ characters. Alphanumeric characters have their ASCII code, but
+ punctuation characters do not. Note that this may be locale-dependent
+ and may not work correctly on international keyboards.
+ @type Class Object
+ @final
+ @private
+*/
+PoorText.keyCodeToFunctionKey = {
+ 8:"backspace", 9:"tab", 13:"enter", 19:"pause", 27:"escape", 32:"space",
+ 33:"pageup", 34:"pagedown", 35:"end", 36:"home", 37:"left", 38:"up",
+ 39:"right", 40:"down", 44:"printscreen", 45:"insert", 46:"delete",
+ 112:"f1", 113:"f2", 114:"f3", 115:"f4", 116:"f5", 117:"f6", 118:"f7",
+ 119:"f8", 120:"f9", 121:"f10", 122:"f11", 123:"f12",
+ 144:"numlock", 145:"scrolllock"
+};
+
+/**
+ This object maps keydown keycode values to key names for printable
+ characters. Alphanumeric characters have their ASCII code, but
+ punctuation characters do not. Note that this may be
+ locale-dependent and may not work correctly on international
+ keyboards.
+ @type Class Object
+ @final
+ @private
+*/
+PoorText.keyCodeToPrintableChar = {
+ 48:"0", 49:"1", 50:"2", 51:"3", 52:"4", 53:"5", 54:"6", 55:"7", 56:"8",
+ 57:"9", 65:"a", 66:"b", 67:"c", 68:"d", 69:"e", 70:"f", 71:"g", 72:"h",
+ 73:"i", 74:"j", 75:"k", 76:"l", 77:"m", 78:"n", 79:"o", 80:"p", 81:"q",
+ 82:"r", 83:"s", 84:"t", 85:"u", 86:"v", 87:"w", 88:"x", 89:"y", 90:"z"
+};
+
+
+PoorText.addHTML = function() {
+ var dlgForm = $('pt-dlg-form-addHTML');
+
+ var tag = $A(dlgForm['tag']).find(function(elm) {
+ return elm.checked;
+ }).value;
+
+ var title = (dlgForm['pt-dlg-tooltip'].value || '');
+ var url = (dlgForm['pt-dlg-url'].value || '');
+ var pt = PoorText.focusedObj;
+ var oldElm = pt.selected.elm;
+
+ // A real link
+ if (tag == 'a') {
+ // A link with the default prompt -> do nothing
+ if (url == 'http://') return;
+ // So we_ve got a url. Modify an existing link?
+ if (oldElm) {
+ // A link with an empty URL -> delete the link
+ if (url == '') {
+ try { oldElm.removeAttribute('class'); } catch(e) {}
+ try { pt.doDeleteHTML(pt.selected.range); } catch(e) {alert(e)}
+ return;
+ }
+ oldElm.setAttribute('href', url);
+ oldElm.setAttribute('_poortext_url', url);
+ if (title) {
+ oldElm.setAttribute('title', title);
+ }
+ // We might turn an existing non-link element
+ // into a link element
+ PoorText.setClass(oldElm, 'pt-a');
+ oldElm.setAttribute('_poortext_tag', 'a');
+ return;
+ } else {
+ if (url == '') return;
+ }
+ }
+ // Another tag
+ else {
+ // So we've got something
+ if (oldElm) {
+ // A acronym or other with no title string -> do nothing
+ if (title == '') {
+ /* Note on Gecko (FF 1.5.0.9, 1.8.0.9) If we don't
+ delete the class attribute before deleting the
+ A-tag, Gecko inserts a
+ <span class="pt-<tagname>">...</span> after deleting
+ the A-tag.
+ */
+ try { oldElm.removeAttribute('class') } catch(e) {}
+ try { pt.doDeleteHTML(pt.selected.range); } catch(e) {alert(e)}
+ return;
+ }
+ // With an unchanged title -> do nothing
+ if (title == oldElm.title && tag == oldElm.tag) {
+ return;
+ }
+ // Set new title on old element
+ oldElm.setAttribute('title', title);
+ oldElm.setAttribute('_poortext_tag', tag);
+ PoorText.setClass(oldElm, 'pt-' + tag);
+ try { oldElm.removeAttribute('_poortext_url') } catch(e) {}
+ oldElm.setAttribute('href', '');
+ return;
+ } else {
+ if (title == '') return;
+ }
+ }
+
+ pt.doAddHTML(tag, url, title, pt.selected.range);
+};
+
+
+/**
+ Popup dialog / help
+*/
+PoorText.Popup = {
+ /**
+ Class object storing position information per popup to always
+ restore it at its previous position.
+ @type Class Object
+ @private
+ */
+ pos : new Object(),
+
+ /**
+ Class method to return (maybe first create) a draggable popup DIV
+ for dialogs.
+ @param {STRING} which The name of the popup used to build its ID
+ @return the initialized and draggable popup
+ @private
+ */
+ get : function(which) {
+
+ var popupID = 'pt-popup-'+which;
+
+ var popup = $(popupID);
+
+ if (!popup) {
+ var popup = $(document.createElement('div'));
+ popup.id = popupID;
+ popup.addClassName('pt-popup');
+ popup.hide();
+ document.body.appendChild(popup);
+
+ // IE, even IE7
+ if (Prototype.Browser.IE) popup.setStyle({position: 'absolute'});
+
+ // make it draggable
+ new Draggable(popupID, {
+ starteffect : function() {},
+ endeffect : function(popup) {
+ var offset = Position.cumulativeOffset(popup);
+ PoorText.Popup.pos[which].deltaX = PoorText.Popup.pos[which].centerX - offset[0];
+ PoorText.Popup.pos[which].deltaY = PoorText.Popup.pos[which].centerY - offset[1];
+ }
+ });
+
+ // All popup handlers
+ Event.observe(popup, 'click', PoorText.Popup.clickHandler);
+ Event.observe(popup, 'keydown', PoorText.Popup.keyDownHandler);
+ Event.observe(popup, 'keyup', PoorText.Popup.keyUpHandler);
+
+ // Remember us
+ PoorText.Popup[which] = popup;
+ }
+
+ return popup;
+ },
+
+ clickHandler : function(event) {
+ var target = $(event.element());
+ var popup = target.up('.pt-popup');
+
+ switch (target.id) {
+ case 'pt-dlg-close':
+ PoorText.Popup.close(popup);
+ break;
+ case 'pt-dlg-cancel':
+ PoorText.Popup.close(popup);
+ break;
+ case 'pt-dlg-ok':
+ PoorText.addHTML(popup);
+ PoorText.Popup.close(popup);
+ break;
+ case 'pt-dlg-a':
+ $('pt-dlg-url-row').show();
+ break;
+ case 'pt-dlg-abbr':
+ $('pt-dlg-url-row').hide();
+ break;
+ case 'pt-dlg-acronym':
+ $('pt-dlg-url-row').hide();
+ break;
+ }
+
+ PoorText.cancelClickOnWindow = true;
+
+ }.bindAsEventListener(PoorText.Popup),
+
+ keyDownHandler : function(event) { // stop IE from beeping
+ if (event.keyCode == 13) { // when pressing KEY_RETURN
+ Event.stop(event);
+ }
+ }.bindAsEventListener(PoorText.Popup),
+
+ keyUpHandler : function(event) {
+ var target = $(event.element());
+ var popup = target.up('.pt-popup');
+
+ switch (target.id) {
+ case 'pt-dlg-url':
+ PoorText.Popup.keyReturnHandler(event, popup);
+ break;
+ case 'pt-dlg-tooltip':
+ PoorText.Popup.keyReturnHandler(event, popup);
+ break;
+ case 'pt-dlg-a':
+ $('pt-dlg-url-row').show();
+ break;
+ case 'pt-dlg-abbr':
+ $('pt-dlg-url-row').hide();
+ break;
+ case 'pt-dlg-acronym':
+ $('pt-dlg-url-row').hide();
+ break;
+ case 'pt-dlg-close':
+ if (event.keyCode == 13) { // KEY_RETURN
+ PoorText.Popup.close(popup);
+ }
+ break;
+ case 'pt-dlg-cancel':
+ if (event.keyCode == 13) { // KEY_RETURN
+ PoorText.Popup.close(popup);
+ }
+ break;
+ case 'pt-dlg-ok':
+ if (event.keyCode == 13) { // KEY_RETURN
+ PoorText.addHTML(popup);
+ PoorText.Popup.close(popup);
+ }
+ break;
+ }
+
+ if (event.keyCode == 27) { // KEY_ESC
+ PoorText.Popup.close(popup);
+ }
+
+ }.bindAsEventListener(PoorText.Popup),
+
+ close : function(popup) {
+ // remove content and hide popup
+ popup.innerHTML = '';
+ popup.hide();
+ this.afterClosePopup();
+ },
+
+ keyReturnHandler : function(event, popup) {
+ if (event.keyCode == 13) { // KEY_RETURN
+ PoorText.addHTML(popup);
+ PoorText.Popup.close(popup);
+ Event.stop(event);
+ }
+ }
+};
+
+/**
+ Initialize popup positions
+*/
+(function() {
+ ['addHTML', 'help'].each(function(which) {
+ PoorText.Popup.pos[which] = {
+ deltaX : 0,
+ deltaY : 0,
+ centerX : 0,
+ centerY : 0,
+ oldPopupW : 0,
+ oldPopupH : 0,
+ center : true
+ }
+ });
+})()
+
+
+
+/**
+ Utility method to transform command names in localized display
+ names (for help screen and tooltips)
+ @param {STRING} command name, e.g. 'bold'
+ @return the display name, e.g. 'Bold' or 'Fett'
+ @type String
+ @private
+*/
+PoorText.cmdToDisplayName = function(cmd) {
+ // translate it
+ return PoorText.L10N.localize(cmd.split('_').invoke('capitalize').join(' '));
+};
+
+/**
+ Utility method producing human readable shortcut information for
+ commands
+ @param {STRING} command name, e.g. 'bold'
+ @return shortcut, e.g. 'Ctrl-B'
+ @type String
+ @private
+*/
+PoorText.cmdToDisplayShortcut = function (pt, cmd) {
+ return $A(pt.shortcutFor[cmd].split('_')).invoke('capitalize').join('-');
+};
+
+/*
+ Button Bar
+*/
+
+/**
+ The button bar is dynamically attached to a PoorText field onFocus,
+ detached onBlur. Attachement is controlled by the config flag
+ 'attachButtonBar' of {@link PoorText#config}.
+ @type Class Object
+ @private
+*/
+PoorText.buttonBar = {
+ /**
+ Flag indicating whether the button bar has already been loaded
+ @type Boolean
+ @private
+ */
+ loaded : false
+};
+/**
+ Class method to attach the button bar to the focused PoorText
+ field. Loads the button bar if it has not yet been loaded.
+ @param {PoorText} Object The object representing the focused PoorText field
+ @return nothing
+ @addon
+*/
+PoorText.buttonBar.attach = function(pt) {
+ // maybe load it
+ if (!PoorText.buttonBar.loaded) {
+ PoorText.buttonBar.load();
+ PoorText.buttonBar.loaded = true;
+ }
+
+ // attach the button bar to editable field, but only show buttons
+ // for per-instance available commands
+ $$('li.pt-btn').each(function(btn) {
+ var id = btn.id.replace('pt-btn-', '');
+
+ // add tooltip with shortcut
+ btn.writeAttribute('title', PoorText.cmdToDisplayName(id) + ': '
+ + PoorText.cmdToDisplayShortcut(pt, id));
+
+ pt.filterAvailableCommands().find(function(cmd) { return id == cmd })
+ ? btn.show()
+ : btn.hide();
+ });
+
+ // Special tooltip for special char button
+ $('pt-btn-specialchars').title = PoorText.cmdToDisplayName('show_specialchars') + ': '
+ + PoorText.cmdToDisplayShortcut(pt, 'specialchars');
+
+ // special char bar (button) needs special consideration
+ if (pt.config.attachSpecialCharBar) {
+ // don't show the button if the special chars bar is visible any way
+ $('pt-btn-specialchars').hide();
+ } else {
+ // show/hide it according to previous state
+ if ($('pt-specialCharBar')) pt.showHideSpecialCharBar();
+ }
+
+ // show the button bar
+ var leftTop = pt.frameNode.cumulativeOffset();
+ $('pt-btnBar').setStyle({left : leftTop[0]+'px', top : leftTop[1]-22+'px'}).show();
+};
+
+/**
+ Class method to load the button bar when first attaching it to some
+ PoorText field.
+ @param None
+ @return nothing
+*/
+PoorText.buttonBar.load = function() {
+ // Attach the btnBar HTML to the body
+ var btnBar = document.createElement('div');
+ Element.extend(btnBar);
+ btnBar.id = 'pt-btnBar';
+ btnBar.insert({top : PoorText.htmlFor.buttonBar}).hide();
+ document.body.appendChild(btnBar);
+
+ // Install an onClick handler on the btnBar to capture button
+ // click events
+ Event.observe($('pt-btnBar'), 'click', function (e) {
+ var target = e.target.parentNode;
+ PoorText.focusedObj.functionFor[target.id.replace('pt-btn-', '')].call(PoorText.focusedObj);
+
+ // Don't focus the PT element when we popup a dialog
+ if (target.id != 'pt-btn-add_html' && target.id != 'pt-btn-help') {
+ PoorText.focusedObj.focusEditNode()
+ PoorText.focusedObj.updateButtonBar(e);
+ }
+
+ // Make sure the window onClick handler does not see the btn
+ // click event
+ Event.stop(e);
+ }.bindAsEventListener(PoorText.focusedObj), true);
+};
+
+/*
+ Special Char Bar
+*/
+
+/**
+ The special char bar is dynamically attached to a PoorText field
+ onFocus, detached onBlur if the config flag 'attachSpecialCharBar'
+ is true (see {@link PoorText#config}). If this flag is false, a
+ 'Show Special Char Bar' button is added to the button bar allowing
+ to toggle special char bar display.
+ @type Class Object
+ @private
+*/
+PoorText.specialCharBar = {
+ /**
+ Flag indicating whether the specialChar bar has already been loaded
+ @type BOOL
+ @private
+ */
+ loaded : false
+};
+
+/**
+ Class method to ttach the special char bar to the focused PoorText
+ field. Loads the special char bar if it has not yet been loaded.
+ @param {PoorText} Object The object representing the focused PoorText field
+ @return nothing
+ @addon
+*/
+PoorText.specialCharBar.attach = function(pt) {
+ // maybe load it
+ if (!PoorText.specialCharBar.loaded) {
+ PoorText.specialCharBar.load();
+ PoorText.specialCharBar.loaded = true;
+ }
+
+ // attach the button bar to editable field, but only show buttons
+ // for per-instance available commands
+ $$('li.pt-char').each(function(sc) {
+ var id = sc.id.replace('pt-char-', '');
+
+ // add tooltip with shortcut
+ sc.writeAttribute('title', PoorText.cmdToDisplayShortcut(pt, id));
+
+ // show/hide the char
+ pt.config.specialChars.find(function(sc) { return id == sc.key })
+ ? sc.show()
+ : sc.hide();
+ });
+
+ // show it
+ var leftTop = pt.frameNode.cumulativeOffset();
+ var diff = pt.config.attachButtonBar ? 44 : 22;
+ $('pt-specialCharBar').setStyle({left : leftTop[0]+'px', top : leftTop[1]-diff+'px'}).show();
+};
+
+/**
+ Class method to load the special char bar when first attaching it
+ to some PoorText field.
+ @type Class method
+ @param None
+ @return nothing
+*/
+PoorText.specialCharBar.load = function() {
+ // Attach the specialCharBar HTML to the body
+ var scBar = document.createElement('div');
+ Element.extend(scBar);
+ scBar.id = 'pt-specialCharBar';
+ scBar.insert({top : PoorText.htmlFor.specialCharBar}).hide();
+ document.body.appendChild(scBar);
+
+ // Install an onClick handler on the btnBar to capture button
+ // click events
+ Event.observe($('pt-specialCharBar'), 'click', function (e) {
+ // insert the special char
+ var target = e.target.parentNode;
+ PoorText.focusedObj.insertHTML(PoorText.focusedObj.config.specialChars.get(target.id.replace('pt-char-', '')),
+ true);
+
+ // focus the edit area again
+ PoorText.focusedObj.window.focus();
+
+ // Make sure the main window onClick handler does not see the
+ // special char click event
+ Event.stop(e);
+ return false;
+ }.bindAsEventListener(PoorText.focusedObj), true);
+};
+
+/**
+ The localization object. Contains lexicons and two localization
+ methods.
+ @type Class Object
+ @private
+*/
+PoorText.L10N = {};
+
+/**
+ Class method to localize strings. If {@link PoorText#config.lang}
+ is 'en' return as is.
+ @param {STRING] string to be localized
+ @return {STRING} localized string
+ @private
+*/
+PoorText.L10N.localize = function(orig) {
+ // short-circuit for English
+ if (PoorText.config.lang == 'en') return orig;
+
+ // translate it
+ return PoorText.L10N[PoorText.config.lang][orig];
+};
+
+/**
+ Class method to localize bracket-enclosed strings in dialogs
+ @param {STRING} HTML dialog containing bracketed strings to be localized
+ @return {STRING} HTML dialog with bracketed strings being
+ localized; if {@link PoorText#config.lang} is 'en' just remove the
+ brackets.
+ @private
+*/
+PoorText.L10N.localizeDialog = function(dlg) {
+ // short-circuit for English
+ if (PoorText.config.lang == 'en') return dlg.replace(/\[([^\]]+)\]/g, "$1");
+
+ // translate it
+ return dlg.replace(/\[([^\]]+)\]/g, function(match, captured) {
+ return PoorText.L10N.localize(captured)
+ });
+};
+
+/**
+ Array of basic markup tag names.
+ @type Array
+ @private
+*/
+PoorText.markupButtons = $A([
+ 'bold',
+ 'italic',
+ 'strikethrough',
+ 'subscript',
+ 'superscript',
+ 'underline'
+]);
+
+/** @fileoverview PoorText loader. Takes care of loading what the
+ current browser needs.
+*/
+
+//
+(function() {
+ var scripts = document.getElementsByTagName('script');
+ for (var i=0; i<scripts.length; i++) {
+ var src = scripts[i].getAttribute('src');
+ if (!src) continue;
+ var index = src.indexOf('poortext.js');
+ if (index != -1) {
+ var scriptRoot = src.substring(0, index);
+ }
+ }
+
+ //////////////////////////
+ //
+ // Push on the browser-specific JS
+ //
+ if (Prototype.Browser.IE) {
+ ptScript = 'poortext_ie.js';
+ } else if (Prototype.Browser.Gecko) {
+ ptScript = 'poortext_gecko.js';
+ } else if (Prototype.Browser.WebKit) {
+ ptScript = 'poortext_webkit.js';
+ }
+
+ //////////////////////////
+ //
+ // Load browser-specific version
+ //
+ var scriptTag = document.createElement("script");
+ scriptTag.setAttribute("type","text/javascript");
+ scriptTag.setAttribute("src", scriptRoot+ptScript);
+ document.getElementsByTagName("head")[0].appendChild(scriptTag);
+})()
+/** @fileoverview
+ JS HTML strings to build popups and buttons bars.
+*/
+
+/**
+ Object mapping names to HTML strings representing addHTML dialog,
+ help screen and button bar
+ @return the mapping object
+ @type Class Object
+ @private
+*/
+PoorText.htmlFor = {
+
+ addHTML :
+
+'<div class="pt-popup-content" id="pt-popup-content-addHTML">'
+ +'<form class="pt-dlg-form" id="pt-dlg-form-addHTML">'
+ +'<div class="pt-fieldset">'
+ +'<div class="pt-legend">[Insert]</div>'
+ +'<table>'
+ +'<colgroup>'
+ +'<col class="pt-dlg-first-col">'
+ +'<col class="pt-dlg-second-col">'
+ +'</colgroup>'
+ +'<thead>'
+ +'</thead>'
+ +'<tbody>'
+ +'<tr>'
+ +'<td><input type="radio" name="tag" class="pt-dlg-radio" id="pt-dlg-abbr" value="abbr"/></td>'
+ +'<td><label for="pt-dlg-abbr">[Abbreviation]</label></td>'
+ +'</tr>'
+ +'<tr>'
+ +'<td><input type="radio" name="tag" class="pt-dlg-radio" id="pt-dlg-acronym" value="acronym"/></td>'
+ +'<td><label for="pt-dlg-acronym">[Acronym]</label></td>'
+ +'</tr>'
+ +'<tr>'
+ +'<td><input type="radio" name="tag" class="pt-dlg-radio" id="pt-dlg-a" value="a" checked="checked"/></td>'
+ +'<td><label for="pt-dlg-a">[Link]</label></td>'
+ +'</tr>'
+ +'<tr id="pt-dlg-url-row" style="height:32px">'
+ +'<td><label for="pt-dlg-url">[URL]</label></td>'
+ +'<td><div><input type="text" name="pt-dlg-url" class="pt-dlg-text" id="pt-dlg-url" size="35"/></div></td>'
+ +'</tr>'
+ +'<tr>'
+ +'<td><label for="pt-dlg-tooltip">[Title]</label></td>'
+ +'<td><div><input type="text" name="pt-dlg-tooltip" class="pt-dlg-text" id="pt-dlg-tooltip" size="35"/></div></td>'
+ +'</tr>'
+ +'<tr><td> </td>'
+ +'<td style="text-align: right; height: 28px; vertical-align: bottom">'
+ +'<input type="button" value="[OK]" id="pt-dlg-ok" class="pt-dlg-button">'
+ +'<input type="button" value="[Cancel]" id="pt-dlg-cancel" class="pt-dlg-button">'
+ +'</td>'
+ +'</tr>'
+ +'</tbody>'
+ +'</table>'
+ +'</div>'
+ +'</form>'
++'</div>',
+
+
+ help :
+
+'<div class="pt-popup-content" id="pt-popup-content-help">'
+ +'<div class="pt-fieldset">'
+ +'<div class="pt-legend">[Shortcuts]</div>'
+ +'<table>'
+ +'<tbody>'
+ +'HERE'
+ +'</tbody>'
+ +'</table>'
+ +'</div>'
++'</div>',
+
+
+ buttonBar :
+
+'<ul>'
+ +'<li class="pt-btn" id="pt-btn-bold">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-italic">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-underline">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-strikethrough">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-subscript">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-superscript">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-align_left">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-align_center">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-align_right">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-justify">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-indent">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-outdent">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-add_html">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-delete_html">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-cut">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-copy">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-paste">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-undo">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-redo">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+
+ +'<li class="pt-btn" id="pt-btn-specialchars">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
+ +'<li class="pt-btn" id="pt-btn-help">'
+ +'<a href="javascript:void(0)" class="pt-btnLink"></a>'
+ +'</li>'
++'<ul>',
+
+
+ specialCharBar :
+
+'<ul>'
+ +'<li class="pt-char" id="pt-char-ldquo">'
+ +'<a href="javascript:void(0)" class="pt-charLink"></a>'
+ +'</li>'
+ +'<li class="pt-char" id="pt-char-rdquo">'
+ +'<a href="javascript:void(0)" class="pt-charLink"></a>'
+ +'</li>'
+ +'<li class="pt-char" id="pt-char-lsquo">'
+ +'<a href="javascript:void(0)" class="pt-charLink"></a>'
+ +'</li>'
+ +'<li class="pt-char" id="pt-char-rsquo">'
+ +'<a href="javascript:void(0)" class="pt-charLink"></a>'
+ +'</li>'
+ +'<li class="pt-char" id="pt-char-ndash">'
+ +'<a href="javascript:void(0)" class="pt-charLink"></a>'
+ +'</li>'
++'</ul>'
+
+};
+// -*- mode: c-mode; coding: utf-8 -*-
+PoorText.L10N.de = {
+ "Abbreviation" : "Abkürzung",
+ "Acronym" : "Akronym",
+ "Add Html" : "HTML einfügen",
+ "Bold" : "Fett",
+ "Cancel" : "Abbrechen",
+ "Close" : "Schließen",
+ "Copy" : "Kopieren",
+ "Cut" : "Ausschneiden",
+ "Delete Html" : "HTML löschen",
+ "Help" : "Hilfe",
+ "Hide Specialchars" : "Sonderzeichen ausblenden",
+ "Insert" : "Einfügen",
+ "Italic" : "Kursiv",
+ "Link" : "Link",
+ "OK" : "OK",
+ "Paste" : "Einfügen",
+ "Redo" : "Wiederholen",
+ "Shortcuts" : "Tastenkürzel",
+ "Show Specialchars" : "Sonderzeichen einblenden",
+ "Specialchars" : "Sonderzeichen",
+ "Strikethrough" : "Durchstreichen",
+ "Subscript" : "Tiefstellen",
+ "Superscript" : "Hochstellen",
+ "Title" : "Tooltip",
+ "Toggle Selectall" : "Markieren E/A",
+ "Underline" : "Unterstreichen",
+ "Undo" : "Rückgängig",
+ "URL" : "URL",
+ "Align Left" : "Linksbündig",
+ "Align Center" : "Zentriert",
+ "Align Right" : "Rechtsbündig",
+ "Justify" : "Blocksatz",
+ "Indent" : "Einrücken",
+ "Outdent" : "Ausrücken",
+ "You didn't select any HTML element to delete!" : "Sie haben kein HTML Element zum Löschen markiert!",
+ "You have to select some text to insert a HTML element." : "Markieren Sie zunächst Text, um ein HTML Element einzufügen.",
+ "The 'ENTER' key you pressed is not allowed in this context!" : "Die ENTER Taste kann in diesem Textfeld nicht verwendet werden."
+};
Added: trunk/krang/htdocs/poortext/poortext_gecko.js
===================================================================
--- trunk/krang/htdocs/poortext/poortext_gecko.js (rev 0)
+++ trunk/krang/htdocs/poortext/poortext_gecko.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,728 @@
+/** @fileoverview
+ Gecko specific code
+*/
+
+/**
+ The opening part of the HTML string written to the iframe document
+ when using iframes. It ends right before the closing HEAD tag.
+ @type Class String
+*/
+PoorText.iframeSrcStart = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"><html><head><style type="text/css">body { margin:0; padding:0 }</style>';
+
+/**
+ The closing part of the HTML string written to the iframe
+ document. It begins with the closing HEAD tag and ends with the
+ closing HTML tag.
+ @type Class String
+*/
+PoorText.iframeSrcEnd = '</head><body><br/></body></html>';
+
+/**
+ Array of 'framing' CSS properties we copy from the {@link
+ #srcElement} to {@link #frameNode}.
+ @type Class Array
+ @private
+*/
+PoorText.iframeStyles = [
+ 'top', 'left', 'bottom', 'right', 'zIndex',
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
+ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle',
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
+ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft'
+];
+
+/**
+ Array of markup CSS properties we copy from {@link #srcElement} to
+ {@link #styleNode}.
+ @type Class Array
+ @private
+*/
+PoorText.bodyStyles = [
+ 'backgroundColor', 'color', 'width',
+ 'lineHeight', 'textAlign', 'textIndent',
+ 'letterSpacing', 'wordSpacing', 'textDecoration', 'textTransform',
+ 'fontFamily', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight',
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'
+];
+
+/**
+ Allowed HTML tags. Used by {@link PoorText#_cleanPaste} to verify
+ that external text pasted in does not mess up our HTML.
+ @type Class RegExp
+ @final
+ @private
+*/
+PoorText.allowedTagsRE = /^(a|abbr|acronym|b|br|em|i|strike|strong|sub|sup|u)$/i;
+
+/**
+ Regexp matching {@link PoorText#bodyStyles} which get copied from {@link
+ #srcElement} to {@link #styleNode}.
+ @type RegExp
+ @private
+ @final
+*/
+PoorText.styleRE = '';
+(function() {
+ PoorText.styleRE = new RegExp(PoorText.bodyStyles.join('|'));
+})();
+
+// Gecko specific output filtering
+/**@ignore*/
+PoorText.outFilterGecko = function(node, isTest) {
+
+ var html;
+
+ if (isTest) {
+ html = node;
+ } else {
+ html = node.innerHTML;
+ }
+
+ // replace <b> with <strong>
+ html = html.replace(/<(\/?)b(\s|>|\/)/ig, "<$1strong$2")
+
+ // replace <i> with <em>
+ .replace(/<(\/?)i(\s|>|\/)/ig, "<$1em$2")
+
+ return html;
+};
+
+/**@ignore*/
+PoorText.outFilters.push(PoorText.outFilterGecko);
+
+/**@ignore*/
+PoorText.inFilterGecko = function(node) {
+ if (/^\s*$/.test(node.innerHTML)) { // make cursor visible
+ node.innerHTML = '<br>';
+ return node;
+ }
+
+ var html = node.innerHTML;
+
+ // replace <strong> with <b>
+ html = html.replace(/<(\/?)strong(\s|>|\/)/ig, "<$1b$2")
+
+ // replace <em> with <i>
+ .replace(/<(\/?)em(\s|>|\/)/ig, "<$1i$2")
+
+ node.innerHTML = html;
+
+ return node;
+}
+
+/**@ignore*/
+PoorText.inFilters.push(PoorText.inFilterGecko);
+
+Object.extend(PoorText.prototype, {
+
+ /**
+ The element we want to make editable must be a DIV. No text(area)
+ elements allowed here.<br>
+ If option deferIframeCreate is false, the iframe will be created on
+ object creation.<br>
+ If option deferIframeCreate is true, it will be created onMouseover
+ the DIV. In this case, the Iframe will only be activated when
+ clicking the DIV.
+ @param none
+ @return nothing
+ @private
+ */
+ makeEditable : function () {
+ var srcElement = this.srcElement;
+
+ if (this.config.deferIframeCreation) {
+ // Using mouse
+ srcElement.addEventListener('mouseover', this._makeEditable.bindAsEventListener(this), false);
+ } else {
+ this._makeEditable();
+ }
+ },
+
+/**@ignore*/
+ _makeEditable : function (event) {
+ if (event) Event.stop(event);
+ // Make sure we are only run once when called as mouseover event
+ // Just removing the event listener doesn't happen soon enough
+ if (this.isLoading) return;
+ this.isLoading = true;
+
+ // srcElement's styles
+ this.ptStyles = window.getComputedStyle(this.srcElement, null);
+
+ // make sure srcElement has non-transparent background
+ if (this.srcElement.getStyle('backgroundColor').toLowerCase() == 'transparent') {
+ this.srcElement.setStyle({backgroundColor : '#fff'});
+ }
+
+ this._prepareContainerAndCreateIframe();
+
+ this._initIframe();
+ },
+
+/**@ignore*/
+ _prepareContainerAndCreateIframe : function() {
+ var ptStyles = this.ptStyles;
+ // Make container for ptElement and iframe
+ var container = document.createElement('div');
+ container.id = this.id+'_container';
+ container.style.top = ptStyles.top;
+ container.style.left = ptStyles.left;
+ container.style.cssFloat = ptStyles.cssFloat;
+ var position = ptStyles.position;
+ container.style.position = position == 'static' ? 'relative' : position;
+ var marginTop = parseInt(ptStyles.marginTop);
+ var marginRight = parseInt(ptStyles.marginRight);
+ var marginBottom = parseInt(ptStyles.marginBottom);
+ var marginLeft = parseInt(ptStyles.marginLeft);
+ var borderTop = parseInt(ptStyles.borderTopWidth);
+ var borderRight = parseInt(ptStyles.borderRightWidth);
+ var borderBottom = parseInt(ptStyles.borderBottomWidth);
+ var borderLeft = parseInt(ptStyles.borderLeftWidth);
+ var paddingTop = parseInt(ptStyles.paddingTop);
+ var paddingRight = parseInt(ptStyles.paddingRight);
+ var paddingBottom = parseInt(ptStyles.paddingBottom);
+ var paddingLeft = parseInt(ptStyles.paddingLeft);
+ var width = parseInt(ptStyles.width);
+ var height = parseInt(ptStyles.height);
+ container.style.width = (marginLeft + borderLeft + paddingLeft
+ + width
+ + paddingRight + borderRight + marginRight) + 'px';
+ container.style.height = (marginTop + borderTop + paddingTop
+ + height
+ + paddingBottom + borderBottom + marginBottom) + 'px';
+ // Put ptElement inside the container
+ var srcElement = this.srcElement;
+ srcElement = srcElement.parentNode.replaceChild(container, srcElement);
+ container.appendChild(srcElement);
+ srcElement.style.position = 'absolute';
+ srcElement.style.top = '0px';
+ srcElement.style.left = '0px';
+ srcElement.style.width = width + 'px';
+ srcElement.style.height = height + 'px';
+
+ // Create Iframe
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('id', this.id+'_iframe');
+
+ // Style it like the DIV
+ iframe.style.width = width + paddingLeft + paddingRight + 'px';
+ iframe.style.height = height + paddingTop + paddingBottom + 'px';
+ PoorText.iframeStyles.each(function(style) {
+ iframe.style[style] = ptStyles[style];
+ });
+
+ // Place it below the DIV
+ iframe.style.position = 'absolute';
+ container.insertBefore(iframe, srcElement);
+ this.iframe = iframe;
+ },
+
+ /**@ignore*/
+ _initIframe : function () {
+
+ if (this.isLoaded) return;
+
+ // Try to init the iframe until init succeeds
+ try {
+ this.document = this.iframe.contentDocument;
+ this.window = this.iframe.contentWindow;
+
+ if (!this.document) {
+ setTimeout(function() {this._initIframe()}.bind(this), 50);
+ return false;
+ }
+ } catch (e) {
+ setTimeout(function() {this._initIframe()}.bind(this), 50);
+ return false;
+ }
+
+ this.isLoaded = true;
+
+ // Write the the HTML structure
+ var doc = this.document;
+ doc.open();
+ doc.write(PoorText.iframeSrcStart
+ +this.config.iframeHead
+ +PoorText.iframeSrcEnd
+ );
+ doc.close();
+
+ this._finishIframe();
+ },
+
+ /**@ignore*/
+ _finishIframe : function() {
+
+ if (!this.document.body || !this.document.body.replaceChild) {
+ setTimeout( function() { this._finishIframe() }.bind(this), 50);
+ return false;
+ } else {
+ // Create EditNode
+ this._createEditNode();
+
+ // Apply inFilters
+ this.setHtml(this.srcElement, PoorText.inFilters);
+
+ // Finally make it editable
+ this.document.designMode = 'on';
+
+ // The Node to style attributes enumerated in PoorText.bodyStyles
+ this.styleNode = this.document.body;
+
+ // The Node to style attributes enumerated in PoorText.iframeStyles
+ this.frameNode = this.iframe;
+
+ // The Node receiving events
+ this.eventNode = this.document;
+
+ // Hook in default events
+ var events = PoorText.events;
+ for (type in events) {
+ this.observe(type, 'builtin', this[events[type]], true);
+ }
+
+ // Don't use SPAN tags for markup
+ try {
+ this.document.execCommand('styleWithCSS', false, false);
+ }
+ catch(e) {
+ this.document.execCommand('useCSS', false, true);
+ }
+
+ // Style the iframe's body like the DIV
+ var ptStyles = this.ptStyles;
+ var body = this.document.body;
+ PoorText.bodyStyles.each(function(style) {
+ body.style[style] = ptStyles[style];
+ });
+
+ // Install iframe activation handlers
+ if (this.config.deferIframeCreation) {
+ Event.observe(this.srcElement, 'click',
+ this._activateIframe.bindAsEventListener(this), true);
+ } else {
+ this._activateIframe();
+ }
+ }
+ },
+
+ _createEditNode : function() {
+ var editNode = document.createElement('div');
+ var body = this.document.body;
+ body.replaceChild(editNode, body.firstChild);
+ this.editNode = body.firstChild;
+ this.editNode.id = 'pt-edit-node';
+ },
+
+ _recreateEditNode : function() {
+ // create the editNode DIV as the first body child
+ this._createEditNode();
+
+ // care for text flavor fields
+ if (this.config.type == 'text') this.editNode.setAttribute('class', 'pt-text-height');
+
+ // add the break
+ var br = document.createElement('br');
+ this.editNode.appendChild(br);
+
+ // When recreating, place cursor within editNode and
+ // before the break
+ var range = this.window.getSelection().getRangeAt(0);
+ range.selectNode(br);
+ range.collapse(true); // collapse to range start
+ },
+
+ /**@ignore*/
+ _activateIframe : function (event) {
+ if (event) Event.stop(event);
+ var srcElement = this.srcElement;
+ if (srcElement) {
+ if (this.config.deferIframeCreation) {
+ Event.stopObserving(this.srcElement, 'click');
+ }
+ srcElement.hide();
+ this.window.focus();
+ }
+ },
+
+ /**@ignore*/
+ getStyle : function (style) {
+ var node = PoorText.styleRE.test(style) ? this.styleNode : this.frameNode;
+ return Element.getStyle((node || this.srcElement), style);
+ },
+
+ /**@ignore*/
+ setStyle : function(css) {
+ for (var attr in css) {
+ attr = attr.camelize();
+ if (PoorText.styleRE.test(attr)) {
+ this.styleNode.style[attr] = css[attr];
+ } else {
+ this.frameNode.style[attr] = css[attr];
+ }
+ }
+ },
+
+ /**@ignore*/
+ getLink : function () {
+ var elm = '';
+ var sel = this.window.getSelection();
+ var range = sel.getRangeAt(0);
+
+ if (sel == '') {
+ // Are we placed within a unselected elm
+ if (elm = this._getLinkFromInside(range.commonAncestorContainer)) {
+ sel.selectAllChildren(elm);
+ return {elm : elm};
+ }
+ else {
+ return {msg : 'showAlert'};
+ }
+ }
+
+ // Try dblclick selection first (case 13)
+ if (!elm) elm = this._getLinkFromOutside(sel, range.commonAncestorContainer.childNodes);
+
+ // Try |<a>...|</a> (case 15, 17, 23, 24)
+ if (!elm) elm = this._getLinkFromInside(range.endContainer.parentNode);
+
+ // Try |<a>...</a>|, beginning in TextNode, ending in TextNode, a-tag is in between
+ // (case 16, 18, 20, 22,
+ if (!elm) elm = this._getLinkFromOutside(sel, sel.anchorNode.parentNode.childNodes);
+
+ // Try <a>|...|</a> (case 14, 19, 21)
+ if (!elm) elm = this._getLinkFromInside(range.startContainer);
+
+ return {elm : elm};
+ },
+
+ /**@ignore*/
+ _getLinkFromInside : function(a) {
+ while (a) {
+ if (a.nodeName.toLowerCase() == 'a') return a;
+ a = a.parentNode;
+ }
+ return null;
+ },
+
+ /**@ignore*/
+ _getLinkFromOutside : function(sel, children) {
+ for (i = 0; i < children.length; i++) {
+ var child = children[i];
+ if ((sel.containsNode(child, false)) && (child.nodeName.toLowerCase() == 'a')) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ storeSelection : function(range) {
+ if (!range) range = this.window.getSelection().getRangeAt(0);
+ this.selection = range;
+ },
+
+ restoreSelection : function(range) {
+ if (!range) range = this.selection;
+ var selection = this.window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ },
+
+ // Stolen from FCKeditor
+ /**@ignore*/
+ doAddHTML :function (tag, url, title) {
+ // Delete old elm
+ this.document.execCommand("unlink", false, null);
+
+ // Generate a temporary name for the elm.
+ var tmpUrl = 'javascript:void(0);/*' + ( new Date().getTime() ) + '*/' ;
+
+ // Use the internal "CreateLink" command to create the link.
+ this.document.execCommand('createlink', false, tmpUrl);
+
+ // Retrieve the just created link using XPath.
+ var elm = this.document.evaluate("//a[@href='" + tmpUrl + "']",
+ this.document.body, null, 9, null).singleNodeValue ;
+
+ if (elm) {
+ if (tag == 'a') {
+ elm.href = url;
+ elm.setAttribute('_poortext_url', url);
+ }
+ else {
+ elm.setAttribute('href', '');
+ }
+ elm.setAttribute('_poortext_tag', tag);
+ PoorText.setClass(elm, 'pt-' + tag);
+ elm.setAttribute('title', title);
+ }
+ return elm;
+ },
+
+ /**@ignore*/
+ doDeleteHTML :function() {
+ this.document.execCommand('unlink', false, null);
+ this.window.getSelection().collapseToEnd();
+ },
+
+ /**
+ Dropin replacement for execCommand('selectall'). Unlike
+ 'selectall', this method toggles the selection. It also implements
+ some hackery to avoid that the editNode DIV gets removed, if
+ toggleSelectAll() is followed by execCommand('cut').
+ @returns true
+ @private
+ */
+ toggleSelectAll : function() {
+ var selection = this.window.getSelection();
+ var range;
+
+ if (this.selectedAll) {
+ // restore the cursor position
+ this.restoreSelection(this.selectedAllSelection);
+
+ // clean up
+ this.selectedAll = false;
+ this.stopObserving('click', 'toggleSelectAll');
+ this.stopObserving('keypress', 'toggleSelectAll');
+ }
+ else {
+ // store the cursor position
+ this.selectedAllSelection = selection.getRangeAt(0);
+
+ // select the editNode's children
+ range = this.document.createRange();
+ range.selectNodeContents(this.editNode);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ this.selectedAll = true;
+
+ this.observe('click',
+ 'toggleSelectAll',
+ function() {
+ this.toggleSelectAll();
+ }, true);
+
+ this.observe('keypress',
+ 'toggleSelectAll',
+ function(event) {
+ if (event.ctrlKey == true) return true;
+ // Let the default action be taken
+ setTimeout(function() {this.toggleSelectAll()}.bind(this), 1);
+ }, true);
+ }
+ return true;
+ },
+
+ /**@ignore*/
+ markup : function(cmd) {
+ this.document.execCommand(cmd, false, null);
+ },
+
+ /**@ignore*/
+ focusEditNode : function() {
+ this.window.focus();
+ },
+
+ /**@ignore*/
+ selectionCollapseToEnd : function () {
+ var selection = this.window.getSelection();
+ if (selection) selection.collapseToEnd();
+ },
+
+ /**
+ Method called onKeyUp to do browser-specific things.
+ @param Object PoorText
+ @return none
+ @private
+ */
+ afterOnKeyUp : function(event) {
+ },
+
+ /**@ignore*/
+ filterAvailableCommands : function () {
+ return this.config.availableCommands.select(function(cmd) {
+ return ! /cut|copy|paste/.test(cmd);
+ });
+ },
+
+ insertHTML : function(html, viaButton) {
+ this.document.execCommand('insertHTML', false, html);
+ },
+
+ undo : function() {
+ this.document.execCommand('undo', false, null);
+ var sel = this.getLink();
+ if (sel.elm) {
+ PoorText.setClass(sel.elm, 'pt-'+sel.elm.getAttribute('_poortext_tag'));
+ }
+ },
+
+ // when pressing DOWN remove last BR when previousSibling is *not* a BR
+ _down : function() {
+ var lastElement = this.editNode.lastChild;
+ if (lastElement
+ && lastElement.nodeName.toLowerCase() == 'br'
+ && lastElement.previousSibling.nodeName.toLowerCase() != 'br'
+ ) {
+ this.editNode.removeChild(lastElement);
+ }
+ },
+
+ afterShowHideSpecialCharBar : function() {
+ },
+
+ __afterKeyDispatch : function(keyname) {
+ // Clean pasted text
+ if (keyname == 'ctrl_v') {
+ setTimeout(function() {
+ this.applyFiltersTo(this.editNode, PoorText.pasteFilters);
+ }.bind(this), 1);
+ }
+ // tame the cursor at line end
+ if (keyname == 'down') {
+ setTimeout(function() {
+ this._down();
+ }.bind(this), 10);
+ }
+ // make sure we always have an editNode
+ if (keyname == 'backspace' || keyname == 'delete') {
+ if (this.styleNode.firstChild.nodeName.toLowerCase() == 'br') {
+ this._recreateEditNode();
+ }
+ }
+ },
+
+});
+
+/**
+ Clean the pasted text: If a node is of an allowed type, leave it
+ alone. If it is not allowed (pasted text comes from the outside
+ world), replace it with its text content.<br>
+ <br>Exceptions from this rule:</b>
+ If node is a DIV, replace it with its children.<br>
+ If node is a P, insert two BR before it and replace node with its children.
+ @param {Node} node This is our editNode
+ @return true if editNode was clean, false otherwise
+ @type Boolean
+ @private
+*/
+PoorText.blockLevelPasteFilter = function(editNode) {
+
+ // Begin with the editNode's children
+ var nodes = $A(editNode.childNodes);
+
+ while (nodes && nodes.length) {
+ var node = nodes.pop();
+
+ // only consider HTMLElement nodes
+ if (node && node.nodeType == 1) {
+ var tagName = node.tagName.toLowerCase();
+
+ // replace <div> with its children
+ if (tagName == 'div') {
+ PoorText.replace_with_children(node, nodes);
+ continue;
+ }
+
+ // replace <p> with <br/><br/>
+ if (tagName == 'p') {
+ node.parentNode.insertBefore(document.createElement('br'), node);
+ node.parentNode.insertBefore(document.createElement('br'), node);
+ PoorText.replace_with_children(node, nodes);
+ continue;
+ }
+
+ // chop all other tags
+ if (! PoorText.allowedTagsRE.test(node.nodeName)) {
+ // disallowed node -> replace with its textContent
+ var parent = node.parentNode;
+
+ if (parent) {
+ var text = node.textContent;
+ var textNode = document.createTextNode(text);
+ parent.replaceChild(textNode, node);
+ }
+ continue;
+ }
+
+ // consider the children
+ if (node.hasChildNodes()) {
+ $A(node.childNodes).each(function(n) {
+ if (n && n.nodeType == 1) nodes.push(n);
+ });
+ }
+ }
+ }
+
+ return editNode;
+};
+
+PoorText.replace_with_children = function(node, nodes) {
+ var parent = node.parentNode;
+ $A(node.childNodes).each(function(child) {
+ parent.insertBefore(child, node);
+ nodes.push(child);
+ });
+ parent.removeChild(node);
+}
+
+PoorText.pasteFilters = [
+ PoorText.blockLevelPasteFilter,
+ PoorText.inFilterGecko,
+ PoorText.addUrlProtection
+];
+
+/**@ignore*/
+Object.extend(PoorText.Popup, {
+ positionIt : function(popup, which) {
+ // With fixed position
+ if (PoorText.Popup.pos[which].center) {
+ // center on popup creation
+ PoorText.Popup.pos[which].center = false;
+
+ centerX = Math.round(window.innerWidth / 2)
+ - (popup.offsetWidth / 2) + 'px';
+
+ centerY = Math.round(window.innerHeight/ 2)
+ - (popup.offsetHeight / 2) + 'px';
+
+ popup.setStyle({left: centerX, top: centerY});
+ }
+ },
+
+ afterClosePopup : function() {
+ setTimeout(function() {
+ /* When called by the keydown handler of the URL or TITLE
+ field in this.addHTMLDialog(), this.closePopup
+ triggers the following error in FF < 2.0 (1.8.1).
+
+ [Exception... "'Permission denied to set property
+ XULElement.selectedIndex' when calling method:
+ [nsIAutoCompletePopup::selectedIndex]" nsresult: "0x8057001e
+ (NS_ERROR_XPC_JS_THREW_STRING)" location: ...
+ */
+ try { PoorText.focusedObj.window.focus() } catch(e) {} // keep Gecko happy
+ }.bind(this), 50);
+ }
+});
+
+PoorText.getHref = function(element) {
+ return element.getAttribute('href');
+}
+
+PoorText.setClass = function(elm, className) {
+ elm.setAttribute('class', className);
+}
+
+/**
+ onDOMContentLoader for Mozilla/Opera
+*/
+if (document.addEventListener) {
+ document.addEventListener(
+ "DOMContentLoaded",
+ PoorText.onload,
+ false);
+}
Added: trunk/krang/htdocs/poortext/poortext_ie.js
===================================================================
--- trunk/krang/htdocs/poortext/poortext_ie.js (rev 0)
+++ trunk/krang/htdocs/poortext/poortext_ie.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,463 @@
+/** @fileoverview
+ MSIE specific code
+*/
+
+PoorText.iframeStyles = [
+ 'top', 'left', 'bottom', 'right', 'zIndex',
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
+ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle',
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
+ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft'
+];
+
+/**
+ Array of markup CSS properties we copy from {@link #srcElement} to
+ {@link #styleNode}.
+ @type Class Array
+ @private
+*/
+PoorText.bodyStyles = [
+ 'backgroundColor', 'color', 'width',
+ 'lineHeight', 'textAlign', 'textIndent',
+ 'letterSpacing', 'wordSpacing', 'textDecoration', 'textTransform',
+ 'fontFamily', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight',
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'
+];
+
+/**
+ Allowed HTML tags. Used by {@link PoorText#_cleanPaste} to verify
+ that external text pasted in does not mess up our HTML.
+ @type Class RegExp
+ @final
+ @private
+*/
+PoorText.allowedTagsRE = /^(a|abbr|acronym|b|br|em|i|strike|strong|sub|sup|u)$/i;
+
+/**
+ Regexp matching {@link PoorText#bodyStyles} which get copied from {@link
+ #srcElement} to {@link #styleNode}.
+ @type RegExp
+ @private
+ @final
+*/
+PoorText.styleRE = '';
+(function() {
+ PoorText.styleRE = new RegExp(PoorText.bodyStyles.join('|'));
+})();
+
+// IE specific output filtering
+/**@ignore*/
+PoorText.outFilterIE = function(node, isTest) {
+
+ var html;
+
+ if (isTest) {
+ html = node;
+ } else {
+ html = node.innerHTML;
+ }
+
+ // do whatever you want to html
+
+ return html;
+};
+
+/**@ignore*/
+PoorText.outFilters.push(PoorText.outFilterIE);
+
+/**@ignore*/
+PoorText.inFilterIE = function(node) {
+ if (node.innerHTML == '') return node;
+
+ var html = node.innerHTML;
+
+ // do whatever you want to html
+
+ node.innerHTML = html;
+
+ return node;
+}
+
+/**
+ Cut / Copy / Paste
+
+ When cutting/copying PT-internal text, copy *with* markup.
+
+ When copying from external source, copy text only.
+
+*/
+PoorText.events['cut'] = 'onCut';
+PoorText.events['copy'] = 'onCopy';
+PoorText.events['paste'] = 'onPaste';
+
+/**@ignore*/
+PoorText.inFilters.push(PoorText.inFilterIE);
+
+Object.extend(PoorText.prototype, {
+ /**
+ The element we want to make editable must be a DIV. No text(area)
+ elements allowed here.<br>
+ If option deferIframeCreate is false, the iframe will be created on
+ object creation.<br>
+ If option deferIframeCreate is true, it will be created onMouseover
+ the DIV. In this case, the Iframe will only be activated when
+ clicking the DIV.
+ @param none
+ @return nothing
+ @private
+ */
+ makeEditable : function () {
+ var srcElement = this.srcElement;
+
+ srcElement.contentEditable = true;
+
+ this.editNode = srcElement;
+ this.eventNode = srcElement;
+ this.styleNode = srcElement;
+ this.frameNode = srcElement;
+ this.document = document;
+ this.window = window;
+
+ if (this.config.type == 'text') {
+ var nobr = document.createElement('nobr');
+ srcElement.wrap(nobr);
+ }
+
+ // Filter the input
+ this.setHtml(this.srcElement, PoorText.inFilters);
+
+ // Hook in default events
+ var events = PoorText.events;
+ for (type in events) {
+ this.observe(type, 'builtin', this[events[type]], true);
+ }
+ },
+
+ /**@ignore*/
+ getStyle : function (style) {
+ var node = PoorText.styleRE.test(style) ? this.styleNode : this.frameNode;
+ return Element.getStyle((node || this.srcElement), style);
+ },
+
+ /**@ignore*/
+ setStyle : function(css) {
+ for (var attr in css) {
+ attr = attr.camelize();
+ if (PoorText.styleRE.test(attr)) {
+ this.styleNode.style[attr] = css[attr];
+ } else {
+ this.frameNode.style[attr] = css[attr];
+ }
+ }
+ },
+
+ /**@ignore*/
+ getLink : function () {
+
+ // maybe selected text
+ var text = this.selection.text;
+
+ // are we placed within a link?
+ var elm = this.selection.parentElement();
+ var tagName = elm.tagName.toLowerCase();
+
+ // no text selected and not within a link
+ if (tagName != 'a' && text == '') {
+ return {msg : "showAlert"};
+ }
+
+ // some text selected, but not (only) a link
+ if (tagName != 'a') return {};
+
+ // got an existing link: expand the selection
+ this.selection.moveToElementText(elm);
+
+ return {elm : elm};
+ },
+
+ storeSelection : function(range) {
+ if (!range) range = document.selection.createRange();
+ this.selection = range;
+ },
+
+ restoreSelection : function() {
+ if (this.selection) this.selection.select();
+ },
+
+ // Stolen from FCKeditor
+ /**@ignore*/
+ doAddHTML : function (tag, url, title, range) {
+
+ // where are we?
+ this.restoreSelection();
+
+ // Delete old elm
+ this.document.execCommand("unlink", false, null);
+
+ // Generate a temporary name for the elm.
+ var tmpUrl = 'javascript:void(0);/*' + ( new Date().getTime() ) + '*/' ;
+
+ // Use the internal "CreateLink" command to create the link.
+ this.document.execCommand('createlink', false, tmpUrl);
+
+ // Retrieve the just created link
+ var elm = $$('a').find(function(link) {
+ return PoorText.getHref(link) == tmpUrl;
+ });
+
+ if (elm) {
+ if (tag == 'a') {
+ elm.href = url;
+ elm.setAttribute('_poortext_url', url);
+ }
+ else {
+ elm.setAttribute('href', '');
+ }
+
+ elm.setAttribute('_poortext_tag', tag);
+ PoorText.setClass(elm, 'pt-' + tag);
+ elm.setAttribute('title', title);
+ }
+
+ return elm;
+ },
+
+ /**@ignore*/
+ doDeleteHTML : function(range) {
+
+ // gimmi something to chew
+ this.restoreSelection();
+
+ // delete link
+ document.execCommand('unlink', false, null);
+
+ // goto end of word
+ this.selection.collapse(false);
+ this.restoreSelection();
+ },
+
+ /**
+ Dropin replacement for execCommand('selectall'). Unlike
+ 'selectall', this method toggles the selection. It also makes sure
+ that the cursor position is restored when deselecting.
+ @returns true
+ @private
+ */
+ toggleSelectAll : function() {
+
+ var range = document.selection.createRange();
+
+ if (this.selectedAll) {
+ // restore the cursor position
+ if (this.selectedAllSelection) this.selectedAllSelection.select();
+
+ // reset state
+ this.selectedAll = false;
+ this.stopObserving('click', 'toggleSelectAll');
+ this.stopObserving('keydown', 'toggleSelectAll');
+
+ } else {
+ // remember the cursor position
+ this.selectedAllSelection = document.selection.createRange();
+ this.selectedAll = true;
+
+ // select all
+ document.execCommand('selectAll', false, null);
+
+ // register handlers
+ this.observe('click',
+ 'toggleSelectAll',
+ function(event) {
+ this.toggleSelectAll();
+ Event.stop(event);
+ }, true);
+
+ this.observe('keydown',
+ 'toggleSelectAll',
+ function(e) {
+ if (e.ctrlKey == true) return;
+ // gimmi the focus for edit commands
+ this.editNode.focus();
+ this.selectedAll = false;
+ this.stopObserving('click', 'toggleSelectAll');
+ this.stopObserving('keydown', 'toggleSelectAll');
+ }, true);
+ }
+ return true;
+ },
+
+ /**@ignore*/
+ markup : function(cmd) {
+ // don't use the bookmark here: it messes up cut/copy selections
+ this.editNode.focus();
+
+ // get the selected range
+ var range = document.selection.createRange();
+
+ // mark it up
+ this.document.execCommand(cmd, false, null);
+
+ // make cursor visible again
+ setTimeout(function() {
+ range.select();
+ },1);
+
+ },
+
+ /**@ignore*/
+ selectionCollapseToEnd : function () {
+ // IE collapses selections automatically onBlur.
+ // That's why we do nothing here
+ },
+
+ /**@ignore*/
+ filterAvailableCommands : function () {
+ return this.config.availableCommands;
+ },
+
+ /**
+ Method called onKeyUp to do browser-specific things.
+ @param Object PoorText
+ @return none
+ @private
+ */
+ afterOnKeyUp : function(event) {
+ // store our position
+ this.storeSelection();
+ },
+
+ /**@ignore*/
+ focusEditNode : function() {
+ this.editNode.focus();
+ },
+
+ insertHTML : function(html, viaButton) {
+ var range;
+ if (viaButton) {
+ setTimeout(function() {
+ this.editNode.focus();
+ range = document.selection.createRange();
+ range.pasteHTML(html);
+ }.bind(this),10);
+ } else {
+ range = document.selection.createRange();
+ range.pasteHTML(html);
+ }
+ },
+
+ undo : function() {
+ this.markup('undo');
+ },
+
+ afterShowHideSpecialCharBar : function() {
+ this.editNode.focus();
+ },
+
+ onCut : function(event) {
+ this._onCutCopy(event);
+ },
+
+ onCopy : function(event) {
+ this._onCutCopy(event);
+ },
+
+ _onCutCopy : function(event) {
+ // remember internal cut or copy
+ var range = document.selection.createRange();
+ if (range) {
+ PoorText._internalPasteText = range.text;
+ }
+ },
+
+ onPaste : function(event) {
+ // internal paste: copy with markup
+ var clipText = window.clipboardData.getData('Text');
+ if (clipText == PoorText._internalPasteText) return;
+
+ // extermal paste: copy text only
+ var range = document.selection.createRange();
+ range.pasteHTML(window.clipboardData.getData('Text'));
+ Event.stop(event);
+ }
+});
+
+/**@ignore*/
+Object.extend(PoorText.Popup, {
+ positionIt : function(popup, which) {
+ // Position it absolutely
+ var popup = $('pt-popup-'+which);
+
+ var currPopupW = popup.offsetWidth;
+ var currPopupH = popup.offsetHeight;
+
+ var oldPopupW = PoorText.Popup.pos[which].oldPopupW;
+ var oldPopupH = PoorText.Popup.pos[which].oldPopupH;
+
+ // // Center it if the popup dimensions did change (popup content changed)
+ // if (currPopupW != oldPopupW || currPopupH != oldPopupH) {
+ // PoorText.popupPos[which].deltaX = 0;
+ // PoorText.popupPos[which].deltaY = 0;
+ // }
+
+ var windowDimensions = document.viewport.getDimensions();
+ var scrollOffsets = document.viewport.getScrollOffsets();
+
+ centerX = Math.round(windowDimensions.width / 2)
+ - (currPopupW / 2)
+ + scrollOffsets.left;
+
+ var realX = centerX - PoorText.Popup.pos[which].deltaX + 'px';
+
+ centerY = Math.round(windowDimensions.height / 2)
+ - (currPopupH / 2)
+ + scrollOffsets.top;
+
+ var realY = centerY - PoorText.Popup.pos[which].deltaY + 'px';
+
+ popup.setStyle({left: realX, top: realY});
+
+ PoorText.Popup.pos[which].centerX = centerX;
+ PoorText.Popup.pos[which].centerY = centerY;
+ PoorText.Popup.pos[which].oldPopupW = currPopupW;
+ PoorText.Popup.pos[which].oldPopupH = currPopupH;
+ },
+
+ afterClosePopup : function() {
+ // restore selection
+ if (PoorText.focusedObj) {
+ PoorText.focusedObj.focusEditNode();
+ PoorText.focusedObj.restoreSelection();
+ }
+ }
+});
+
+PoorText.getHref = function(element) {
+ return element.getAttribute('href', 2);
+}
+
+PoorText.__abbrFixIE6 = function (node) {
+ var html = node.innerHTML;
+
+ // replace <abbr> with our special <a> tag
+ html = html.replace(/<abbr title\="?([^\">]+)"?>([^<]+)<\/abbr>/ig,
+ '<a class="pt-abbr" title="$1" _poortext_tag="abbr">$2</a>');
+
+ node.innerHTML = html;
+}
+
+PoorText.__enterKeyHandler = function(pt) {
+ pt.keyHandlerFor['enter'] = function() {
+ this.insertHTML('<br>');
+ document.selection.createRange().select(); // move cursor to the next line
+ };
+}
+
+PoorText.setClass = function(elm, className) {
+ elm.setAttribute('className', className);
+}
+
+document.onreadystatechange = function() {
+ if (document.readyState == 'complete') {
+ PoorText.onload();
+ }
+}
Added: trunk/krang/htdocs/poortext/poortext_webkit.js
===================================================================
--- trunk/krang/htdocs/poortext/poortext_webkit.js (rev 0)
+++ trunk/krang/htdocs/poortext/poortext_webkit.js 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,660 @@
+/** @fileoverview
+ Webkit specific code
+*/
+
+PoorText.iframeStyles = [
+ 'top', 'left', 'bottom', 'right', 'zIndex',
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
+ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle',
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
+ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft'
+];
+
+/**
+ Array of markup CSS properties we copy from {@link #srcElement} to
+ {@link #styleNode}.
+ @type Class Array
+ @private
+*/
+PoorText.bodyStyles = [
+ 'backgroundColor', 'color', 'width',
+ 'lineHeight', 'textAlign', 'textIndent',
+ 'letterSpacing', 'wordSpacing', 'textDecoration', 'textTransform',
+ 'fontFamily', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight',
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'
+];
+
+/**
+ Allowed HTML tags. Used by {@link PoorText#_cleanPaste} to verify
+ that external text pasted in does not mess up our HTML.
+ @type Class RegExp
+ @final
+ @private
+*/
+PoorText.allowedTagsRE = /^(a|abbr|acronym|b|br|del|em|i|span|strike|strong|sub|sup|u)$/i;
+
+/**
+ Regexp matching {@link PoorText#bodyStyles} which get copied from {@link
+ #srcElement} to {@link #styleNode}.
+ @type RegExp
+ @private
+ @final
+*/
+PoorText.styleRE = '';
+(function() {
+ PoorText.styleRE = new RegExp(PoorText.bodyStyles.join('|'));
+})();
+
+PoorText.AppleSpanClassName = 'Apple-style-span';
+
+PoorText.replaceSpan = function(node, createSpan) {
+
+ node.select('.'+PoorText.AppleSpanClassName).each(function(oldNode) {
+
+ // temporally put the replacement elements on a document fragment
+ var docFrag = document.createDocumentFragment();
+ var newNode = docFrag;
+ oldNode = $(oldNode);
+
+ // maybe create new span
+ newNode = createSpan(oldNode, newNode);
+
+ // if a new SPAN has been created...
+ if (docFrag.hasChildNodes()) {
+ // replace old with new, caring for its content
+ newNode.innerHTML = oldNode.innerHTML;
+ oldNode.parentNode.replaceChild(docFrag, oldNode);
+ }
+
+ // recurse into the substitution SPAN
+ var moreSpans;
+ try {
+ // Prototype-1.6 does not expand docFrag with select() method!
+ moreSpans = $(newNode).select('.'+PoorText.AppleSpanClassName);
+ } catch(e) {}
+
+ if (moreSpans && moreSpans.length) {
+ PoorText.replaceSpan(newNode, createSpan);
+ }
+ });
+}
+
+// Webkit specific output filtering
+/**@ignore*/
+PoorText.outFilterWebKit = function(editNode) {
+ return editNode.innerHTML;
+ var spanReplaceMap = $A([
+ // CSS rule value replacement element
+ ['font-weight', 'bold', 'strong'],
+ ['font-style', 'italic', 'em' ],
+ ['text-decoration', 'underline', 'u' ],
+ ['text-decoration', 'line-through', 'strike'],
+ ['vertical-align', 'sub', 'sub' ],
+ ['vertical-align', 'super', 'sup' ]
+ ]);
+
+ // don't alter the live node
+ var cloned = $(editNode.cloneNode(true))
+
+ // replace SPANs with markup tags
+ PoorText.replaceSpan(cloned, function(oldNode, newNode) {
+ spanReplaceMap.each(function(spec) {
+ if (oldNode.getStyle(spec[0]) == spec[1]) {
+ var n = document.createElement(spec[2]);
+ newNode.appendChild(n);
+ newNode = n;
+ }
+ });
+ return newNode;
+ });
+
+ // filter out SPANs without style/class attribs
+ PoorText.replaceSpan(cloned, function(oldSpan, newNode) {
+ if (!oldSpan.hasAttribute('style')) {
+ PoorText.replace_with_children(oldSpan);
+ }
+ return newNode;
+ });
+
+ // filter out remaining double tags, but let through BR (the 'b' in the char class, huh what a kludge!)
+ var html = cloned.innerHTML;
+
+ return html.replace(/(<\/?[^>b]+>)\1/gi, "$1");
+};
+
+/**@ignore*/
+PoorText.outFilters.push(PoorText.outFilterWebKit);
+
+/**@ignore*/
+PoorText.inFilterWebKit = function(node) {
+ if (node.innerHTML == '') return node;
+
+ // setup
+ var elements = $(node).childElements();
+ var interesting = /strong|b|em|i|u|del|strike|sub|sup/i;
+
+ // filter map
+ replaceMap = {
+ strong : {fontWeight : 'bold'},
+ b : {fontWeight : 'bold'},
+ em : {fontStyle : 'italic'},
+ i : {fontStyle : 'italic'},
+ u : {textDecoration : 'underline'},
+ del : {textDecoration : 'line-through'},
+ strike : {textDecoration : 'line-through'},
+ sub : {verticalAlign : 'sub'},
+ sup : {verticalAlign : 'super'}
+ };
+
+ // the workhorse
+ function replaceNodes(orig) {
+ // create the span
+ var span = new Element('span', {'class' : PoorText.AppleSpanClassName}).update(orig[orig.length-1].innerHTML);
+
+ // set the SPAN's style
+ orig.each(function(node) {
+ span.setStyle(replaceMap[node.nodeName.toLowerCase()]);
+ });
+
+ // replace it
+ orig[0].parentNode.replaceChild(span, orig[0]);
+ }
+
+ // Walk the DOM, looking for parent/child markup sequences
+ while (elements && elements.length) {
+ var next = elements.shift();
+
+ // parent/child/... sequences get stored in array
+ if (Object.isArray(next)) {
+ elm = next[next.length-1];
+ acc = next;
+ } else {
+ elm = next;
+ acc = [elm];
+ }
+
+ if (interesting.test(elm.nodeName)) {
+ var firstChild = elm.firstChild;
+
+ if (interesting.test(firstChild.nodeName)) {
+ // interesting child, store in array
+ acc.push(firstChild);
+ // and look for the child's child
+ elements.unshift(acc);
+ } else {
+ // no child: go ahead, replace it
+ replaceNodes(acc);
+ }
+ } else {
+ // not interesting: consider its children
+ elements = elements.concat($(elm.childElements()));
+ }
+ }
+
+ return node;
+}
+
+/**@ignore*/
+//PoorText.inFilters.push(PoorText.inFilterWebKit);
+
+/**
+ Add onPaste event
+*/
+PoorText.events['paste'] = 'onPaste';
+
+Object.extend(PoorText.prototype, {
+ /**
+ The element we want to make editable must be a DIV. No text(area)
+ elements allowed here.<br>
+ If option deferIframeCreate is false, the iframe will be created on
+ object creation.<br>
+ If option deferIframeCreate is true, it will be created onMouseover
+ the DIV. In this case, the Iframe will only be activated when
+ clicking the DIV.
+ @param none
+ @return nothing
+ @private
+ */
+ makeEditable : function () {
+ var srcElement = this.srcElement;
+
+ srcElement.contentEditable = true;
+
+ this.editNode = srcElement;
+ this.eventNode = srcElement;
+ this.styleNode = srcElement;
+ this.frameNode = srcElement;
+ this.document = document;
+ this.window = window;
+
+ // text fields should not wrap the text
+ if (this.config.type == 'text') {
+ var nobr = document.createElement('nobr');
+ srcElement.wrap(nobr);
+ }
+
+ // Filter the input
+ this.setHtml(this.srcElement, PoorText.inFilters);
+
+ // Hook in default events
+ var events = PoorText.events;
+ for (type in events) {
+ this.observe(type, 'builtin', this[events[type]], true);
+ }
+ },
+
+
+ /**@ignore*/
+ getStyle : function (style) {
+ var node = PoorText.styleRE.test(style) ? this.styleNode : this.frameNode;
+ return Element.getStyle((node || this.srcElement), style);
+ },
+
+ /**@ignore*/
+ setStyle : function(css) {
+ for (var attr in css) {
+ attr = attr.camelize();
+ if (PoorText.styleRE.test(attr)) {
+ this.styleNode.style[attr] = css[attr];
+ } else {
+ this.frameNode.style[attr] = css[attr];
+ }
+ }
+ },
+
+ /**@ignore*/
+ getLink : function () {
+ var elm = '';
+ var sel = this.window.getSelection();
+ var range = sel.getRangeAt(0);
+
+ if (sel == '') {
+ // Are we placed within a unselected elm
+ if (elm = this._getLinkFromInside(range.commonAncestorContainer)) {
+ sel.selectAllChildren(elm);
+ this.storeSelection(sel);
+ return {elm : elm};
+ }
+ else {
+ return {msg : 'showAlert'};
+ }
+ }
+
+ // Store selection
+ this.storeSelection(sel);
+
+ // Try dblclick selection first (case 13)
+ if (!elm) elm = this._getLinkFromOutside(sel, range.commonAncestorContainer.childNodes);
+
+ // Try |<a>...|</a> (case 15, 17, 23, 24)
+ if (!elm) elm = this._getLinkFromInside(range.endContainer.parentNode);
+
+ // Try |<a>...</a>|, beginning in TextNode, ending in TextNode, a-tag is in between
+ // (case 16, 18, 20, 22,
+ if (!elm) elm = this._getLinkFromOutside(sel, sel.anchorNode.parentNode.childNodes);
+
+ // Try <a>|...|</a> (case 14, 19, 21)
+ if (!elm) elm = this._getLinkFromInside(range.startContainer);
+
+ return {elm : elm};
+ },
+
+ /**@ignore*/
+ _getLinkFromInside : function(a) {
+ while (a) {
+ if (a.nodeName.toLowerCase() == 'a') return a;
+ a = a.parentNode;
+ }
+ return null;
+ },
+
+ /**@ignore*/
+ _getLinkFromOutside : function(sel, children) {
+ for (i = 0; i < children.length; i++) {
+ var child = children[i];
+ if ((sel.containsNode(child, false)) && (child.nodeName.toLowerCase() == 'a')) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ storeSelection : function(sel) {
+ if (!sel) { sel = window.getSelection(); }
+ if (sel) { this.selection = sel.getRangeAt(0); }
+ },
+
+ restoreSelection : function(range) {
+ if (!range) range = this.selection;
+ var selection = this.window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ },
+
+ // Stolen from FCKeditor
+ /**@ignore*/
+ doAddHTML :function (tag, url, title) {
+ // give execCommand() an object to act upon
+ this.restoreSelection();
+
+ // Delete old elm
+ this.document.execCommand("unlink", false, null);
+
+ // Generate a temporary name for the elm.
+ var tmpUrl = 'javascript:void(0);/*' + ( new Date().getTime() ) + '*/' ;
+
+ // Use the internal "CreateLink" command to create the link.
+ this.document.execCommand('createlink', false, tmpUrl);
+
+ // Retrieve the just created link using XPath.
+ var elm = this.document.evaluate("//a[@href='" + tmpUrl + "']",
+ this.document.body, null, 9, null).singleNodeValue ;
+
+ if (elm) {
+ if (tag == 'a') {
+ elm.href = url;
+ elm.setAttribute('_poortext_url', url);
+ }
+ else {
+ elm.setAttribute('href', '');
+ }
+ elm.setAttribute('_poortext_tag', tag);
+ PoorText.setClass(elm, 'pt-' + tag);
+ elm.setAttribute('title', title);
+ }
+ return elm;
+ },
+
+ /**@ignore*/
+ doDeleteHTML :function() {
+ this.restoreSelection();
+ this.document.execCommand('unlink', false, null);
+ this.window.getSelection().collapseToEnd();
+ },
+
+ /**
+ Dropin replacement for execCommand('selectall'). Unlike
+ 'selectall', this method toggles the selection. It also implements
+ some hackery to avoid that the editNode DIV gets removed, if
+ toggleSelectAll() is followed by execCommand('cut').
+ @returns true
+ @private
+ */
+ toggleSelectAll : function() {
+ var selection = this.window.getSelection();
+ var range;
+
+ if (this.selectedAll) {
+ if (this.selectedAllSelection) {
+ // restore the cursor position
+ this.restoreSelection(this.selectedAllSelection);
+
+ // reset state
+ this.selectedAll = false;
+ this.stopObserving('click', 'toggleSelectAll');
+ this.stopObserving('keypress', 'toggleSelectAll');
+ }
+ }
+ else {
+ // remember the cursor position
+ this.selectedAllSelection = selection.getRangeAt(0);
+ this.selectedAll = true;
+
+ // selectall
+ document.execCommand('selectall', false, null);
+
+ this.observe('click',
+ 'toggleSelectAll',
+ function() {
+ this.toggleSelectAll();
+ }, true);
+
+ this.observe('keypress',
+ 'toggleSelectAll',
+ function(event) {
+ if (event.ctrlKey == true) return true;
+ // Let the default action be taken
+ setTimeout(function() {this.toggleSelectAll()}.bind(this), 1);
+ }, true);
+ }
+ return true;
+ },
+
+ /**@ignore*/
+ markup : function(cmd) {
+ this.restoreSelection();
+ this.document.execCommand(cmd, false, null);
+ },
+
+ /**@ignore*/
+ focusEditNode : function() {
+ this.editNode.focus();
+ },
+
+ /**@ignore*/
+ selectionCollapseToEnd : function () {
+ var selection = this.window.getSelection();
+ if (selection) selection.collapseToEnd();
+ },
+
+ /**
+ Method called onKeyUp to do browser-specific things.
+ @param Object PoorText
+ @return none
+ @private
+ */
+ afterOnKeyUp : function(event) {
+ this.storeSelection();
+ },
+
+ /**@ignore*/
+ filterAvailableCommands : function () {
+ return this.config.availableCommands.select(function(cmd) {
+ return ! /cut|copy|paste/.test(cmd);
+ });
+ },
+
+ insertHTML : function(html, viaButton) {
+ this.document.execCommand('insertHTML', false, html);
+ },
+
+ undo : function() {
+ this.markup('undo');
+ },
+
+ afterShowHideSpecialCharBar : function() {
+ },
+
+ /**
+ Handler called onPaste events
+ @param {EVENT} event
+ @returns nothing
+ @private
+ */
+ onPaste : function(event) {
+ setTimeout(function() {
+ this.applyFiltersTo(this.editNode, PoorText.pasteFilters);
+ this.restoreSelection();
+ }.bind(this), 10);
+ }
+
+});
+
+/**
+ Clean the pasted text: If a node is of an allowed type, leave it
+ alone. If it is not allowed (pasted text comes from the outside
+ world), replace it with its text content.<br>
+ <br>Exceptions from this rule:</b>
+ If node is a DIV, replace it with its children.<br>
+ If node is a P, insert two BR before it and replace node with its children.
+ @param none
+ @return true if editNode was clean, false otherwise
+ @type Boolean
+ @private
+*/
+PoorText.blockLevelPasteFilter = function(editNode) {
+
+ // Begin with the editNode's children
+ var nodes = $A(editNode.childNodes);
+
+ while (nodes && nodes.length) {
+ var node = nodes.shift();
+
+ // only consider HTMLElement nodes
+ if (node && node.nodeType == 1) {
+ var nodeName = node.nodeName.toLowerCase();
+
+ // replace <div> with its children
+ if (nodeName == 'div') {
+ PoorText.replace_with_children(node, nodes);
+ continue;
+ }
+
+ // replace <p> with <br/><br/> plus P's children
+ if (nodeName == 'p') {
+ node.parentNode.insertBefore(document.createElement('br'), node);
+ node.parentNode.insertBefore(document.createElement('br'), node);
+ PoorText.replace_with_children(node, nodes);
+ continue;
+ }
+
+ // chop all other tags
+ if (! PoorText.allowedTagsRE.test(node.nodeName)) {
+ // disallowed node -> replace with its textContent
+ var parent = node.parentNode;
+
+ if (parent) {
+ var text = node.textContent;
+ var textNode = document.createTextNode(text);
+ parent.replaceChild(textNode, node);
+ }
+
+ clean = false;
+ continue;
+ } else if (nodeName == 'a') {
+ // style attrib is inserted on some LINK/ABBR/ACRONYM conversions
+ node.removeAttribute('style');
+ }
+
+ // consider the children
+ if (node.hasChildNodes()) {
+ $A(node.childNodes).each(function(n) {
+ if (n && n.nodeType == 1) nodes.unshift(n);
+ });
+ }
+ }
+ }
+ return editNode;
+}
+
+PoorText.spanFilter = function(editNode) {
+ // clean Apple's extra SPANs
+ var spanStyles = $A([
+ // CSS rule value
+ ['font-weight', 'bold' ],
+ ['font-style', 'italic' ],
+ ['text-decoration', 'underline' ],
+ ['text-decoration', 'line-through'],
+ ['vertical-align', 'sub', ],
+ ['vertical-align', 'super', ]
+ ]);
+
+ // replace with our own inline CSS
+ PoorText.replaceSpan(editNode, function(oldSpan, newNode) {
+
+ var newStyles = 0;
+ var styles = {};
+
+ // create our own inline CSS
+ spanStyles.each(function(spec) {
+ if (oldSpan.getStyle(spec[0]) == spec[1]) {
+ styles[spec[0].camelize()] = spec[1];
+ newStyles = 1;
+ }
+ });
+
+ // get rid of old inline CSS
+ oldSpan.removeAttribute('style');
+
+ // maybe set new style
+ if (newStyles) {
+ // set our styles
+ oldSpan.setStyle(styles);
+ }
+
+ return oldSpan;
+ });
+
+ return editNode;
+}
+
+PoorText.replace_with_children = function(node, nodes) {
+ var parent = node.parentNode;
+ $A(node.childNodes).each(function(child) {
+ parent.insertBefore(child, node);
+ if (nodes) nodes.push(child);
+ });
+ parent.removeChild(node);
+ return parent;
+}
+
+PoorText.pasteFilters = [
+ PoorText.inFilterWebKit,
+// PoorText.spanFilter,
+ PoorText.addUrlProtection,
+ PoorText.blockLevelPasteFilter,
+];
+
+
+/**@ignore*/
+Object.extend(PoorText.Popup, {
+ positionIt : function(popup, which) {
+ // With fixed position
+ if (PoorText.Popup.pos[which].center) {
+ // center on popup creation
+ PoorText.Popup.pos[which].center = false;
+
+ centerX = Math.round(window.innerWidth / 2)
+ - (popup.offsetWidth / 2) + 'px';
+
+ centerY = Math.round(window.innerHeight/ 2)
+ - (popup.offsetHeight / 2) + 'px';
+
+ popup.setStyle({left: centerX, top: centerY});
+ }
+ },
+
+ afterClosePopup : function() {
+ if (PoorText.focusedObj) {
+ PoorText.focusedObj.restoreSelection();
+ }
+ }
+});
+
+PoorText.getHref = function(element) {
+ return element.getAttribute('href');
+}
+
+PoorText.setClass = function(elm, className) {
+ elm.setAttribute('class', className);
+}
+
+PoorText.__enterKeyHandler = function(pt) {
+ pt.keyHandlerFor['enter'] = function() {
+ document.execCommand('insertlinebreak', false, null);
+ };
+}
+
+
+/**
+ onDOMContentLoaded for Safari
+ borrowed from Dean Edwards
+*/
+if (/WebKit/i.test(navigator.userAgent)) { // sniff
+ var _timer = setInterval(function() {
+ if (/loaded|complete/.test(document.readyState)) {
+ PoorText.onload(); // call the onload handler
+ }
+ }, 10);
+}
Added: trunk/krang/lib/Krang/ElementClass/PoorText.pm
===================================================================
--- trunk/krang/lib/Krang/ElementClass/PoorText.pm (rev 0)
+++ trunk/krang/lib/Krang/ElementClass/PoorText.pm 2008-10-23 18:48:08 UTC (rev 5535)
@@ -0,0 +1,556 @@
+package Krang::ElementClass::PoorText;
+use strict;
+use warnings;
+
+use Krang::ClassFactory qw(pkg);
+
+use Krang::ClassLoader base => 'ElementClass';
+
+use Krang::ClassLoader Log => qw(critical);
+use Krang::ClassLoader Message => qw(add_message);
+use Krang::ClassLoader Localization => qw(localize);
+use Krang::ClassLoader 'Markup::Gecko';
+use Krang::ClassLoader 'Markup::IE';
+use Krang::ClassLoader 'Markup::WebKit';
+
+use Digest::MD5 qw(md5_hex);
+use JSON::Any;
+use Carp qw(croak);
+
+use Krang::MethodMaker get_set => [
+ qw(
+ type commands
+ width special_char_bar shortcut_for
+ height command_button_bar indent_size
+ )
+];
+
+our %js_name_for = (
+ type => 'type',
+ commands => 'availableCommands',
+ special_char_bar => 'attachSpecialCharBar',
+ command_button_bar => 'attachButtonBar',
+ shortcut_for => 'shortcutFor',
+ indent_size => 'indentSize',
+);
+
+sub new {
+ my $pkg = shift;
+ my %args = (
+ width => 400,
+ height => 120, # for 'textarea' flavour. For 'text' flavour see poortext.css '.pt-text'
+ command_button_bar => 1,
+ special_char_bar => 0,
+ @_
+ );
+
+ # validate commands spec
+ my $command_spec = $pkg->command_spec();
+ if ($args{commands}) {
+ if (!exists $command_spec->{$args{commands}}) {
+ croak("\"$args{commands}\" is not a known set of commands");
+ }
+ }
+
+ return $pkg->SUPER::new(%args);
+}
+
+sub mark_form_invalid {
+ my ($self, %arg) = @_;
+ my ($html) = @arg{qw(html)};
+ return qq{<div style="border-left: 3px solid #ffffac">$html</div>};
+}
+sub validate { 1 }
+
+sub load_query_data {
+ my ($self, %args) = @_;
+ my ($query, $element) = @args{qw(query element)};
+ my ($param) = $self->param_names(element => $element);
+
+ # the HTML
+ my $html = $query->param($param);
+
+ critical(__PACKAGE__ . "->load_query_data($param) - HTML coming from the browser: " . $html);
+
+ # fix the markup
+ if ($html) {
+ $html = pkg("Markup::$ENV{KRANG_BROWSER_ENGINE}")->browser2db(html => $html);
+
+ critical(__PACKAGE__ . "->load_query_data($param) - HTML sent to DB: " . $html);
+ }
+
+ # the INDENT and ALIGN
+ my $indent = $query->param("${param}_indent");
+ my $align = $query->param("${param}_align");
+
+ $element->data([$html, $indent, $align]);
+}
+
+sub input_form {
+ my ($self, %arg) = @_;
+ my ($query, $element) = @arg{qw(query element)};
+ my ($param) = $self->param_names(element => $element);
+
+ # the returned HTML
+ my $html = "";
+
+ # data has multiple fields: HTML INDENT ALIGN
+ my $data = $element->data;
+ my $text = $data->[0] || $query->param($param) || '';
+ my $indent = $data->[1] || $query->param("${param}_indent") || 0;
+ my $align = $data->[2] || $query->param("${param}_align") || 'left';
+
+ # get some setup stuff
+ my $lang = localize('en');
+ $lang = substr($lang, 0, 2) unless $lang eq 'en';
+ my $install_id = pkg('Info')->install_id();
+ my $config = $self->get_pt_config(%arg);
+ my $class = $self->get_css_class(%arg);
+ my $style = $self->get_css_style(%arg, indent => $indent, align => $align);
+
+ # JavaScript init code: add only once
+ my @sibs = grep { $_->class->isa(__PACKAGE__) } $element->parent()->children();
+ if ($sibs[0]->xpath() eq $element->xpath()) {
+
+ # I''m the first! Insert one-time JavaScript
+ $html .= <<END;
+<script type="text/javascript">
+ // pull in the JavaScript
+if (!Krang.PoorTextLoaded) {
+ // from scriptaculous these two are needed for dialog popups
+ ['effects.js', 'dragdrop.js'].each(function(name) {
+ var script = new Element(
+ 'script',
+ { type: "text/javascript",
+ src : "/static/$install_id/js/" + name}
+ );
+
+ document.body.appendChild(script);
+ });
+
+ // the core poortext.js which will also pull in a browser-specific JavaScript
+ var pt_script = new Element(
+ 'script',
+ { type: "text/javascript",
+ src: "/static/$install_id/poortext/poortext.js"}
+ );
+ document.body.appendChild(pt_script);
+
+ // I tried the same appending procedure for the CSS file poortext/poortext.css,
+ // but for WebKit that comes to late, so I included it in templates/header.base.tmpl
+
+ // make sure we do this only once
+ Krang.PoorTextLoaded = true;
+}
+
+// init function
+poortext_elements = new Array();
+poortext_init = function() {
+ // is poortext.js loaded ?
+ if (typeof PoorText == 'undefined') {
+ setTimeout(poortext_init, 10);
+ return;
+ }
+
+ // deactivate the autoload handler
+ PoorText.autoload = false;
+
+ // is poortext_<browser-specific>.js loaded ?
+ if (typeof PoorText.prototype.makeEditable == 'undefined') {
+ setTimeout(poortext_init, 10);
+ return;
+ }
+
+ // language is a global config
+ PoorText.config = { lang : "$lang" };
+
+ // make them all fields
+ poortext_elements.each(function(pt) {
+ new PoorText(pt[0], pt[1]);
+ });
+
+ // finish with some global stuff
+ PoorText.finish_init();
+}
+
+// call init function
+Krang.onload(function() {
+ poortext_init();
+});
+
+// save away the last focused PoorText field to avoid race conditions
+Krang.ElementEditor.add_save_hook(function() {
+ var pt = PoorText.focusedObj;
+ if (pt) {
+ pt.storeForPostBack();
+ }
+});
+</script>
+END
+ }
+
+ # configure the element
+ my $id = md5_hex($param);
+ $html .= <<END;
+<script type="text/javascript">
+ // configure this element
+ var config = {
+ iframeHead : '<link rel="stylesheet" type="text/css" href="/poortext/poortext.css">'
+ $config
+ };
+ poortext_elements.push(["$id", config]);
+</script>
+END
+
+ # create the edit area DIV and ...
+ $text = pkg("Markup::$ENV{KRANG_BROWSER_ENGINE}")->db2browser(html => $text);
+ $html .= qq{<div class="$class" style="$style" id="$id">} . $text . qq{</div>\n};
+
+ # ... its hidden input field used to return the text
+ $html .= qq[<input type="hidden" name="$param" value='$text' id="${id}_return"/>];
+
+ critical(__PACKAGE__ . "->input_form($param) - HTML sent to the browser: " . $text);
+
+ # the hidden field for text indent
+ $html .= qq[<input type="hidden" name="${param}_indent" value="$indent" id="${id}_indent"/>];
+
+ # the hidden field for text alignment
+ $html .= qq[<input type="hidden" name="${param}_align" value="$align" id="${id}_align"/>];
+
+ return $html;
+}
+
+# we override this method so that it won't escape the HTML
+sub view_data {
+ my ($self, %arg) = @_;
+ my ($element, $data) = @arg{qw(element data)};
+
+ return '' unless $data->[0];
+
+ return $data->[0] if $element->class->type eq 'text';
+
+ return <<END;
+<div style="border-bottom: 1px solid #99999">
+ Indent: $data->[1]px — Text Alignment: $data->[2]
+</div>
+$data->[0]
+END
+}
+
+sub template_data {
+ my ($self, %arg) = @_;
+ my ($element) = @arg{qw(element)};
+
+ my $data = $element->data;
+
+ return $data->[0] if $element->class->type eq 'text';
+
+ return <<END;
+<div style="padding-left: $data->[1]; padding-right: $data->[1]; text-align: $data->[2]">
+ $data->[0]
+</div>
+END
+}
+
+sub freeze_data {
+ my ($self, %arg) = @_;
+ my $element = $arg{element};
+ my $data = $element->data || [];
+ my $sep = $self->get_separator();
+ return join($sep, @$data);
+}
+
+sub thaw_data {
+ my ($self, %arg) = @_;
+ my ($element, $data) = @arg{qw(element data)};
+
+ if ($data) {
+ my $sep = $self->get_separator();
+ return $element->data([split(/\Q$sep/, $data)]);
+ } else {
+
+ # HTML INDENT ALIGN
+ return $element->data(['', 0, 'left']);
+ }
+}
+
+sub get_pt_config {
+ my ($self, %arg) = @_;
+ my ($query, $element) = @arg{qw(query element)};
+
+ my $class = $element->class;
+ my @conf = ();
+
+ # $conf will be part of a JavaScript object litteral
+ for my $c (
+ qw(
+ type
+ special_char_bar shortcut_for
+ command_button_bar indent_size
+ )
+ )
+ {
+
+ my $conf;
+ if ($conf = $class->$c) {
+ if (ref($conf)) {
+ push @conf, "$js_name_for{$c} : " . JSON::Any->objToJson($conf);
+ } else {
+ $conf = '' unless $conf; # make sure '0' evalutes to false in JS
+ push @conf, "$js_name_for{$c} : \"$conf\"";
+ }
+ }
+ }
+
+ # add the commands spec
+ my $cmd = $self->get_command_spec(%arg);
+ push @conf, $cmd if $cmd;
+
+ # stringify
+ my $conf = join(',', @conf);
+
+ # prepend a comma since its gonna be part of a JS object
+ return $conf ? ",\n$conf" : '';
+}
+
+sub get_css_class {
+ my ($self, %arg) = @_;
+
+ # CSS class property
+ return $arg{element}->class->type eq 'text'
+ ? "poortext pt-text"
+ : "poortext";
+}
+
+sub get_css_style {
+ my ($self, %arg) = @_;
+ my $element = $arg{element};
+
+ my $w = $element->class->width;
+
+ #
+ # text flavour
+ #
+ return "width: ${w}px;" if $element->class->type eq 'text';
+
+ #
+ # textarea flavour
+ #
+ #
+ my $h = $element->class->height;
+
+ # width and indent (padding)
+ my $indent = $arg{indent};
+ $w -= ($indent * 2);
+
+ return
+ "width: ${w}px; height: ${h}px; padding-left: ${indent}px; padding-right: ${indent}px; text-align: $arg{align};";
+
+}
+
+sub get_command_spec {
+ my ($self, %arg) = @_;
+
+ my $spec = $arg{element}->class->commands;
+
+ # use the default coming with poortext.js
+ return unless $spec;
+
+ # custom command spec
+ if (ref($spec) eq 'ARRAY') {
+ return 'availableCommands: ' . JSON::Any->objToJson($spec);
+ }
+
+ # cooked spec
+ my $command_spec = $self->command_spec();
+
+ for my $cooked (keys %$command_spec) {
+ if ($spec eq $cooked) {
+ return 'availableCommands: ' . JSON::Any->objToJson($command_spec->{$cooked});
+ }
+ }
+
+ # unknown command spec
+ croak __PACKAGE__ . "->get_command_spec() : Unknow value for key 'commands' -> [ $spec ]";
+}
+
+sub command_spec {
+ my ($self, %arg) = @_;
+
+ # order matters!
+ return {
+ basic => [
+ qw(bold italic underline
+ cut copy paste
+ add_html delete_html
+ redo undo
+ help)
+ ],
+ basic_with_special_chars => [
+ qw(bold italic underline
+ cut copy paste
+ add_html delete_html
+ redo undo
+ specialchars help)
+ ],
+ all => [
+ qw(bold italic underline
+ strikethrough subscript superscript
+ cut copy paste
+ align_left align_center align_right justify
+ indent outdent
+ add_html delete_html
+ redo undo
+ specialchars help)
+ ],
+ };
+}
+
+sub get_separator { return "|" }
+
+=head1 NAME
+
+Krang::ElementClass::PoorText - WYSIWYG element
+
+=head1 SYNOPSIS
+
+ $class = pkg('ElementClass::PoorText')->new(
+ name => "poortext",
+ type => 'textarea',
+ commands => 'all',
+ );
+
+=head1 DESCRIPTION
+
+This element provides a WYSIWYG text editor for HTML by integrating
+with the PoorText WYSIWYG element
+
+=head1 INTERFACE
+
+All the normal L<Krang::ElementClass> attributes are available, plus:
+
+=over 4
+
+=item type
+
+May be 'text' or 'textarea' to mimic the two flavors of legacy text
+input and textarea fields.
+
+=item width
+
+The width of the edit area. Defaults to 400px.
+
+=item height
+
+The height of the edit area. For type 'textarea' defaults to 120px.
+For type 'text' this option takes no effect.
+
+=item commands
+
+This can be either a string denoting a pre-cooked set of WYSIWYG commands
+or an array or command names. The pre-cooked sets are:
+
+ basic => [ qw(bold italic underline
+ cut copy paste
+ add_html delete_html
+ redo undo
+ help)
+ ],
+ basic_with_special_chars => [ qw(bold italic underline
+ cut copy paste
+ add_html delete_html
+ redo undo
+ specialchars help)
+ ],
+ all => [ qw(bold italic underline
+ strikethrough subscript superscript
+ cut copy paste
+ align_left align_center align_right justify
+ indent outdent
+ add_html delete_html
+ redo undo
+ specialchars help) ],
+
+Most of these commands should be self-evident. Some, however, are not:
+
+=over
+
+=item add_html
+
+This command opens a popup allowing to wrap the selected text with a
+A, ABBR or ACRONYM tag.
+
+=item delete_html
+
+Deletes an A, ABBR or ACRONYM around the current selection.
+
+=item specialchars
+
+This command displays a second toolbar allowing to insert double and
+single curly quotes as well as the ndash.
+
+=back
+
+=item command_button_bar
+
+This boolean determines whether a button bar with the configured
+commands will be displayed when the edit area receives focus. (You
+might want to just use the shortcuts. Which ones? Press Ctrl-h). The
+default is true.
+
+=item special_char_bar
+
+If true, the specialchar button bar will be displayed when the edit
+area receives focus. In this case, even if the command button bar is
+configured to contain the specialchars command (the omega sign), this
+command will not be present. Defaults to false.
+
+=item shortcut_for
+
+This hashref maps commands and specialchars to shortcuts. The default
+is:
+
+ bold => 'ctrl_b',
+ italic => 'ctrl_i',
+ underline => 'ctrl_u',
+ subscript => 'ctrl_d',
+ superscript => 'ctrl_s',
+ strikethrough => 'ctrl_t',
+ toggle_selectall => 'ctrl_a',
+ add_html => 'ctrl_l',
+ delete_html => 'ctrl_shift_l',
+ redo => 'ctrl_y',
+ undo => 'ctrl_z',
+ help => 'ctrl_h',
+ cut => 'ctrl_x',
+ copy => 'ctrl_c',
+ paste => 'ctrl_v',
+ specialchars => 'ctrl_6',
+ align_left => 'ctrl_q',
+ align_center => 'ctrl_e',
+ align_right => 'ctrl_r',
+ justify => 'ctrl_w',
+ indent => 'tab',
+ outdent => 'shift_tab',
+ lsquo => 'ctrl_4',
+ rsquo => 'ctrl_5',
+ ldquo => 'ctrl_2',
+ rdquo => 'ctrl_3',
+ ndash => 'ctrl_0',
+
+(See F<htdocs/poortext/poortext_core.js> for more information)
+
+=item indent_size
+
+If the commands 'indent' and 'outdent' are configured, this option
+specifies the number of pixels to indent and outdent. Defaults to 20.
+
+=back
+
+=head1 SEE ALSO
+
+The PoorText source in F<htdocs/poortext/>.
+
+=cut
+
+1;
Modified: trunk/krang/t/publish/page.tmpl
===================================================================
--- trunk/krang/t/publish/page.tmpl 2008-10-23 17:52:13 UTC (rev 5534)
+++ trunk/krang/t/publish/page.tmpl 2008-10-23 18:48:08 UTC (rev 5535)
@@ -2,6 +2,8 @@
<tmpl_if is_wide_page><tmpl_if wide_page>THIS IS A VERY WIDE PAGE<tmpl_else>A NARROW PAGE</tmpl_if></tmpl_if>
<tmpl_if is_header><tmpl_var header></tmpl_if>
<tmpl_if is_paragraph><p><tmpl_var paragraph></p></tmpl_if>
+<tmpl_if is_poortext_header><tmpl_var poortext_header></tmpl_if>
+<tmpl_if is_poortext_paragraph><tmpl_var poortext_paragraph></tmpl_if>
</tmpl_loop>
<P>Page number <tmpl_if current_page_number><tmpl_var current_page_number><tmpl_else>1</tmpl_if> of <tmpl_if total_pages><tmpl_var total_pages><tmpl_else>1</tmpl_if>.</p>
Modified: trunk/krang/templates/header.base.tmpl
===================================================================
--- trunk/krang/templates/header.base.tmpl 2008-10-23 17:52:13 UTC (rev 5534)
+++ trunk/krang/templates/header.base.tmpl 2008-10-23 18:48:08 UTC (rev 5535)
@@ -89,6 +89,8 @@
<!--<script src="/static/<tmpl_var krang_install_id>/js/firebug/firebug.js" type="text/javascript"></script>-->
<script src="/static/<tmpl_var krang_install_id>/codepress/codepress.js" type="text/javascript"></script>
+<link rel="stylesheet" type="text/css" href="/poortext/poortext.css">
+
<script type="text/javascript">
Krang.instance = '<tmpl_var escape=js instance_display_name>';
Krang.Window.init('<tmpl_var window_id>');
|