right-rails 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2685 @@
1
+ /**
2
+ * RightJS-UI RTE v2.2.0
3
+ * http://rightjs.org/ui/rte
4
+ *
5
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
6
+ */
7
+ var Rte = RightJS.Rte = (function(RightJS, document, window) {
8
+ /**
9
+ * This module defines the basic widgets constructor
10
+ * it creates an abstract proxy with the common functionality
11
+ * which then we reuse and override in the actual widgets
12
+ *
13
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
14
+ */
15
+
16
+ /**
17
+ * The widget units constructor
18
+ *
19
+ * @param String tag-name or Object methods
20
+ * @param Object methods
21
+ * @return Widget wrapper
22
+ */
23
+ function Widget(tag_name, methods) {
24
+ if (!methods) {
25
+ methods = tag_name;
26
+ tag_name = 'DIV';
27
+ }
28
+
29
+ /**
30
+ * An Abstract Widget Unit
31
+ *
32
+ * Copyright (C) 2010 Nikolay Nemshilov
33
+ */
34
+ var AbstractWidget = new RightJS.Class(RightJS.Element.Wrappers[tag_name] || RightJS.Element, {
35
+ /**
36
+ * The common constructor
37
+ *
38
+ * @param Object options
39
+ * @param String optional tag name
40
+ * @return void
41
+ */
42
+ initialize: function(key, options) {
43
+ this.key = key;
44
+ var args = [{'class': 'rui-' + key}];
45
+
46
+ // those two have different constructors
47
+ if (!(this instanceof RightJS.Input || this instanceof RightJS.Form)) {
48
+ args.unshift(tag_name);
49
+ }
50
+ this.$super.apply(this, args);
51
+
52
+ if (RightJS.isString(options)) {
53
+ options = RightJS.$(options);
54
+ }
55
+
56
+ // if the options is another element then
57
+ // try to dynamically rewrap it with our widget
58
+ if (options instanceof RightJS.Element) {
59
+ this._ = options._;
60
+ if ('$listeners' in options) {
61
+ options.$listeners = options.$listeners;
62
+ }
63
+ options = {};
64
+ }
65
+ this.setOptions(options, this);
66
+
67
+ return (RightJS.Wrapper.Cache[RightJS.$uid(this._)] = this);
68
+ },
69
+
70
+ // protected
71
+
72
+ /**
73
+ * Catches the options
74
+ *
75
+ * @param Object user-options
76
+ * @param Element element with contextual options
77
+ * @return void
78
+ */
79
+ setOptions: function(options, element) {
80
+ if (element) {
81
+ options = RightJS.Object.merge(options, new Function("return "+(
82
+ element.get('data-'+ this.key) || '{}'
83
+ ))());
84
+ }
85
+
86
+ if (options) {
87
+ RightJS.Options.setOptions.call(this, RightJS.Object.merge(this.options, options));
88
+ }
89
+
90
+ return this;
91
+ }
92
+ });
93
+
94
+ /**
95
+ * Creating the actual widget class
96
+ *
97
+ */
98
+ var Klass = new RightJS.Class(AbstractWidget, methods);
99
+
100
+ // creating the widget related shortcuts
101
+ RightJS.Observer.createShortcuts(Klass.prototype, Klass.EVENTS || RightJS([]));
102
+
103
+ return Klass;
104
+ }
105
+
106
+
107
+ /**
108
+ * RTE's initialization script
109
+ *
110
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
111
+ */
112
+
113
+ var R = RightJS,
114
+ $ = RightJS.$,
115
+ $$ = RightJS.$$,
116
+ $w = RightJS.$w,
117
+ $E = RightJS.$E,
118
+ $A = RightJS.$A,
119
+ isArray = RightJS.isArray,
120
+ RegExp = RightJS.RegExp,
121
+ Class = RightJS.Class,
122
+ Element = RightJS.Element,
123
+ Input = RightJS.Input;
124
+
125
+
126
+
127
+
128
+ /**
129
+ * The Right Text Editor
130
+ *
131
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
132
+ */
133
+ var Rte = new Widget({
134
+
135
+ extend: {
136
+ version: '2.2.0',
137
+
138
+ EVENTS: $w('change focus blur'),
139
+
140
+ // checking if the 'contentEditable' feature is supported at all
141
+ supported: 'contentEditable' in document.createElement('div'),
142
+
143
+ Options: {
144
+ toolbar: 'small', // toolbar, the name or an array of your own
145
+
146
+ autoresize: true, // automatically resize the editor's height to fit the text
147
+
148
+ showToolbar: true, // show the toolbar
149
+ showStatus: true, // show the status bar
150
+
151
+ videoSize: '425x344', // flash-video blocks default size
152
+
153
+ cssRule: 'textarea[data-rte]'
154
+ },
155
+
156
+ // predefined toolbars set
157
+ Toolbars: {
158
+ small: ['Bold Italic Underline Strike Ttext|Cut Copy Paste|Header Code Quote|Link Image Video|Source'],
159
+ basic: [
160
+ 'Save Clear|Cut Copy Paste|Bold Italic Underline Strike Ttext|Left Center Right Justify',
161
+ 'Undo Redo|Header Code Quote|Link Image Video|Dotlist Numlist|Indent Outdent|Source'
162
+ ],
163
+ extra: [
164
+ 'Save Clear|Cut Copy Paste|Bold Italic Underline Strike Ttext|Left Center Right Justify',
165
+ 'Undo Redo|Header Code Quote|Link Image Video|Subscript Superscript|Dotlist Numlist|Indent Outdent',
166
+ 'Format|Fontname Fontsize|Forecolor Backcolor|Source'
167
+ ]
168
+ },
169
+
170
+ Tools: {}, // the index of available tools will be here
171
+
172
+ // the keyboard bindings
173
+ Shortcuts: {
174
+ Bold: 'b',
175
+ Italic: 'i',
176
+ Underline: 'u',
177
+ // Ttext: 't',
178
+ Header: 'h',
179
+ Link: 'l',
180
+ Cut: 'x',
181
+ Copy: 'c',
182
+ Paste: 'v',
183
+ Undo: 'z',
184
+ Redo: 'shift+z',
185
+ Source: 'e',
186
+ // Quote: 'q',
187
+ Code: 'p',
188
+ Save: 's'
189
+ },
190
+
191
+ // tags used by default with formatting tools
192
+ Tags: {
193
+ Bold: 'b',
194
+ Italic: 'i',
195
+ Underline: 'u',
196
+ Strike: 's',
197
+ Ttext: 'tt',
198
+ Code: 'pre',
199
+ Quote: 'blockquote',
200
+ Header: 'h2'
201
+ },
202
+
203
+ // the formatting options, you can use simply tag names
204
+ // or you can also specify tag + class like 'div.blue'
205
+ Formats: {
206
+ 'h1': 'Header 1',
207
+ 'h2': 'Header 2',
208
+ 'h3': 'Header 3',
209
+ 'h4': 'Header 4',
210
+ 'p': 'Paragraph',
211
+ 'pre': 'Preformatted',
212
+ 'blockquote': 'Blockquote',
213
+ 'tt': 'Typetext',
214
+ 'address': 'Address'
215
+ },
216
+
217
+ // the font-name options
218
+ FontNames: {
219
+ 'Andale Mono': 'andale mono,times',
220
+ 'Arial': 'arial,helvetica,sans-serif',
221
+ 'Arial Black': 'arial black,avant garde',
222
+ 'Book Antiqua': 'book antiqua,palatino',
223
+ 'Comic Sans MS': 'comic sans ms,sans-serif',
224
+ 'Courier New': 'courier new,courier',
225
+ 'Georgia': 'georgia,palatino',
226
+ 'Helvetica': 'helvetica',
227
+ 'Impact': 'impact,chicago',
228
+ 'Symbol': 'symbol',
229
+ 'Tahoma': 'tahoma,arial,helvetica,sans-serif',
230
+ 'Terminal': 'terminal,monaco',
231
+ 'Times New Roman': 'times new roman,times',
232
+ 'Trebuchet MS': 'trebuchet ms,geneva',
233
+ 'Verdana': 'verdana,geneva',
234
+ 'Webdings': 'webdings',
235
+ 'Wingdings': 'wingdings,zapf dingbats'
236
+ },
237
+
238
+ // the font-size options
239
+ FontSizes: '6pt 7pt 8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 36pt',
240
+
241
+ Videos: [
242
+ // supported swf video resources
243
+ [/(http:\/\/.*?youtube\.[a-z]+)\/watch\?v=([^&]+)/, '$1/v/$2'],
244
+ [/(http:\/\/video.google.com)\/videoplay\?docid=([^&]+)/, '$1/googleplayer.swf?docId=$2'],
245
+ [/(http:\/\/vimeo\.[a-z]+)\/([0-9]+).*?/, '$1/moogaloop.swf?clip_id=$2']
246
+ ],
247
+
248
+ i18n: {
249
+ Clear: 'Clear',
250
+ Save: 'Save',
251
+ Source: 'Source',
252
+ Bold: 'Bold',
253
+ Italic: 'Italic',
254
+ Underline: 'Underline',
255
+ Strike: 'Strike through',
256
+ Ttext: 'Typetext',
257
+ Header: 'Header',
258
+ Cut: 'Cut',
259
+ Copy: 'Copy',
260
+ Paste: 'Paste',
261
+ Left: 'Left',
262
+ Center: 'Center',
263
+ Right: 'Right',
264
+ Justify: 'Justify',
265
+ Undo: 'Undo',
266
+ Redo: 'Redo',
267
+ Code: 'Code block',
268
+ Quote: 'Block quote',
269
+ Link: 'Add link',
270
+ Image: 'Insert image',
271
+ Video: 'Insert video',
272
+ Dotlist: 'List with dots',
273
+ Numlist: 'List with numbers',
274
+ Indent: 'Indent',
275
+ Outdent: 'Outdent',
276
+ Forecolor: 'Text color',
277
+ Backcolor: 'Background color',
278
+ Select: 'Select',
279
+ Remove: 'Remove',
280
+ Format: 'Format',
281
+ Fontname: 'Font name',
282
+ Fontsize: 'Size',
283
+ Subscript: 'Subscript',
284
+ Superscript: 'Superscript',
285
+ UrlAddress: 'URL Address'
286
+ },
287
+
288
+ current: null
289
+ },
290
+
291
+ /**
292
+ * Basic constructor
293
+ *
294
+ * @param Input textarea reference
295
+ * @param Object additional options
296
+ * @return void
297
+ */
298
+ initialize: function(textarea, options) {
299
+ this
300
+ .$super('rte', {})
301
+ .setOptions(options, textarea)
302
+ .append(
303
+ this.toolbar = new Rte.Toolbar(this),
304
+ this.editor = new Rte.Editor(this),
305
+ this.status = new Rte.Status(this)
306
+ );
307
+
308
+ if (!this.options.showToolbar) {
309
+ this.toolbar.hide();
310
+ }
311
+
312
+ if (!this.options.showStatus) {
313
+ this.status.hide();
314
+ }
315
+
316
+ if (textarea) {
317
+ this.assignTo(textarea);
318
+ }
319
+
320
+ this.undoer = new Rte.Undoer(this);
321
+ this.selection = new Rte.Selection(this);
322
+
323
+ // updating the initial state
324
+ this.selection.exec('styleWithCss', false);
325
+ this.status.update();
326
+ },
327
+
328
+ /**
329
+ * Sets the value
330
+ *
331
+ * @param String value
332
+ * @return Rte this
333
+ */
334
+ setValue: function(value) {
335
+ if (this.textarea) {
336
+ this.textarea.value(value);
337
+ }
338
+ this.editor.update(value);
339
+ return this;
340
+ },
341
+
342
+ /**
343
+ * Returns the current value
344
+ *
345
+ * @return String current value
346
+ */
347
+ getValue: function() {
348
+ return this.editor._.innerHTML;
349
+ },
350
+
351
+ /**
352
+ * Bidirectional method to set/get the value
353
+ *
354
+ * @param String value
355
+ * @return Rte this or String value
356
+ */
357
+ value: function(value) {
358
+ return this[value === undefined ? 'getValue' : 'setValue'](value);
359
+ },
360
+
361
+ /**
362
+ * Disables the editor
363
+ *
364
+ * @return Rte this
365
+ */
366
+ disable: function() {
367
+ this.disabled = true;
368
+ return this.addClass('rui-rte-disabled');
369
+ },
370
+
371
+ /**
372
+ * Enables the editor
373
+ *
374
+ * @return Rte this
375
+ */
376
+ enable: function() {
377
+ this.disabled = false;
378
+ return this.removeClass('rui-rte-disabled');
379
+ },
380
+
381
+ /**
382
+ * Puts the focus into the editor
383
+ *
384
+ * @return Rte this
385
+ */
386
+ focus: function() {
387
+ if (Rte.current !== this) {
388
+ Rte.current = this;
389
+ this.editor.focus();
390
+ }
391
+
392
+ return this;
393
+ },
394
+
395
+ /**
396
+ * Looses focus from the editor
397
+ *
398
+ * @return Rte this
399
+ */
400
+ blur: function() {
401
+ Rte.current = null;
402
+
403
+ this.editor.blur();
404
+
405
+ return this;
406
+ },
407
+
408
+ /**
409
+ * Assigns this Rte to work with this textarea
410
+ *
411
+ * @param mixed textarea reference
412
+ * @return Rte this
413
+ */
414
+ assignTo: function(element) {
415
+ var textarea = $(element),
416
+ size = textarea.size();
417
+
418
+ // displaying self only if the 'contentEditable' feature is supported
419
+ // otherwise keeping original textarea where it is
420
+ if (Rte.supported) {
421
+ this.insertTo(textarea.setStyle(
422
+ 'position:absolute;left:-9999em;'
423
+ ), 'before');
424
+
425
+ this.editor.resize(size);
426
+ this.setWidth(size.x);
427
+
428
+ if (this.options.autoresize) {
429
+ this.editor.setStyle({
430
+ minHeight: size.y + 'px',
431
+ height: 'auto'
432
+ });
433
+ }
434
+ } else {
435
+ textarea.setStyle('visibility:visible');
436
+ }
437
+
438
+ this.setValue(textarea.value());
439
+ this.onChange(function() {
440
+ textarea._.value = this.editor._.innerHTML;
441
+ });
442
+
443
+ this.textarea = textarea;
444
+
445
+ return this;
446
+ }
447
+
448
+ });
449
+
450
+ /**
451
+ * Rte's toolbar unit
452
+ *
453
+ * Copyright (C) 2010 Nikolay Nemshilov
454
+ */
455
+ Rte.Toolbar = new Class(Element, {
456
+
457
+ initialize: function(rte) {
458
+ this.$super('div', {'class': 'rui-rte-toolbar'});
459
+
460
+ this.rte = rte;
461
+ rte.tools = {};
462
+
463
+ var options = rte.options, toolbar = options.toolbar;
464
+
465
+ R(Rte.Toolbars[toolbar] || (isArray(toolbar) ? toolbar : [toolbar])).each(function(line_s) {
466
+ var line = $E('div', {'class': 'line'}).insertTo(this);
467
+
468
+ R(line_s.split('|')).each(function(bar_s) {
469
+ if (!R(bar_s).blank()) {
470
+ var bar = $E('div', {'class': 'bar'}).insertTo(line);
471
+
472
+ R(bar_s.split(' ')).each(function(tool) {
473
+ tool = R(tool).capitalize();
474
+ bar.insert(new Rte.Tools[tool](rte));
475
+ });
476
+ }
477
+ });
478
+ }, this);
479
+
480
+ // adding hidden undo/redo tools if they are not on the toolbar
481
+ // so that the undoer kicked in on the keybindings
482
+ rte.tools.Undo || new Rte.Tools.Undo(rte);
483
+ rte.tools.Redo || new Rte.Tools.Redo(rte);
484
+ },
485
+
486
+ /**
487
+ * Finds a tool for the keyboard event
488
+ *
489
+ * @param {Event} event
490
+ * @return {Rte.Tool} wired tool or false
491
+ */
492
+ shortcut: function(event) {
493
+ var raw = event._, key, tool;
494
+
495
+ for (key in this.rte.tools) {
496
+ tool = this.rte.tools[key];
497
+
498
+ if (tool.shortcut === raw.keyCode && tool.shiftKey === raw.shiftKey) {
499
+ return tool;
500
+ }
501
+ }
502
+
503
+ return null;
504
+ }
505
+
506
+ });
507
+
508
+ /**
509
+ * The actual Editor unit for the Rte
510
+ *
511
+ * Copyright (C) 2010 Nikolay Nemshilov
512
+ */
513
+ Rte.Editor = new Class(Element, {
514
+
515
+ /**
516
+ * Basic constructor
517
+ *
518
+ * @param Rte rte
519
+ * @return void
520
+ */
521
+ initialize: function(rte) {
522
+ // IE won't allow us to set 'contenteditable' progarmatically
523
+ // so we put it as a textual content and then find it manually
524
+ this.$super(rte
525
+ .append('<div contenteditable="true" class="rui-rte-editor"></div>')
526
+ .first('div.rui-rte-editor')._
527
+ );
528
+
529
+ this.rte = rte;
530
+
531
+ this.on({
532
+ focus: this._focus,
533
+ blur: this._blur,
534
+ mouseup: this._mouseup,
535
+ keypress: this._keypress,
536
+ keydown: this._keydown,
537
+ keyup: this._keyup
538
+ });
539
+ },
540
+
541
+ /**
542
+ * Updates the editor's content
543
+ *
544
+ * @param String text
545
+ * @return Rte.Editor this
546
+ */
547
+ update: function(text) {
548
+ this.$super(text);
549
+ return this;
550
+ },
551
+
552
+ /**
553
+ * puts focus on the editing area
554
+ *
555
+ * @return Rte.Editor this
556
+ */
557
+ focus: function() {
558
+ this._.focus();
559
+ return this;
560
+ },
561
+
562
+ /**
563
+ * removes focus out of the editing area
564
+ *
565
+ * @return Rte.Editor this
566
+ */
567
+ blur: function() {
568
+ this._.blur();
569
+ return this;
570
+ },
571
+
572
+ /**
573
+ * Removes the element from the editor, placing all its content
574
+ * in its place
575
+ *
576
+ * @param raw dom element
577
+ * @return void
578
+ */
579
+ removeElement: function(element) {
580
+ if (element !== null) {
581
+ var parent = element.parentNode;
582
+ while (element.firstChild) {
583
+ parent.insertBefore(element.firstChild, element);
584
+ }
585
+ parent.removeChild(element);
586
+ }
587
+ },
588
+
589
+ // protected
590
+
591
+ _focus: function() {
592
+ this.rte.selection.restore();
593
+ this.rte.status.update();
594
+ this.rte.focused = true;
595
+ },
596
+
597
+ _blur: function() {
598
+ this.rte.focused = false;
599
+ this.rte.status.update();
600
+ },
601
+
602
+ _mouseup: function() {
603
+ this._focus();
604
+ },
605
+
606
+ _keypress: function(event) {
607
+ if (this.__stopped) {
608
+ event.stop();
609
+ }
610
+ },
611
+
612
+ _keydown: function(event) {
613
+ var raw = event._, stopped = false, tool;
614
+
615
+ if (raw.metaKey || raw.ctrlKey) {
616
+ if ((tool = this.rte.toolbar.shortcut(event))) {
617
+ tool.call(event);
618
+ }
619
+
620
+ stopped = event.stopped;
621
+ }
622
+
623
+ // an internal marker to lock the 'keypress' event later on
624
+ this.__stopped = stopped;
625
+ },
626
+
627
+ _keyup: function(event) {
628
+ switch (event.keyCode) {
629
+ case 37: // arrow
630
+ case 38: // arrow
631
+ case 39: // arrow
632
+ case 40: // arrow
633
+ this.rte.status.update();
634
+ break;
635
+
636
+ default:
637
+ // watching the typing pauses to fire 'change' events
638
+ var rte = this.rte, editor = this._;
639
+
640
+ if (this._timer !== false) { window.clearTimeout(this._timer); }
641
+
642
+ this._timer = window.setTimeout(function() {
643
+ if (rte.__old_value !== editor.innerHTML) {
644
+ rte.__old_value = editor.innerHTML;
645
+ rte.fire('change');
646
+ }
647
+ }, this._delay);
648
+ }
649
+ },
650
+
651
+ _timer: false,
652
+ _delay: 400
653
+
654
+ });
655
+
656
+ /**
657
+ * The Rte's status bar block
658
+ *
659
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
660
+ */
661
+ Rte.Status = new Class(Element, {
662
+
663
+ /**
664
+ * Basic constructor
665
+ *
666
+ * @param Rte
667
+ * @return void
668
+ */
669
+ initialize: function(rte) {
670
+ this.$super('div', {'class': 'rui-rte-status'});
671
+ this.rte = rte;
672
+ this.nodes = [];
673
+ this.tags = [];
674
+
675
+ this.onMousedown(this._mousedown);
676
+ },
677
+
678
+ /**
679
+ * Updates the current status
680
+ *
681
+ * @return Rte.Status this
682
+ */
683
+ update: function() {
684
+ this._findNodes();
685
+ this._checkTools();
686
+
687
+ return this.$super(this.nodes.map(function(node, index) {
688
+ var name = node.tagName.toLowerCase();
689
+
690
+ if (node.id) {
691
+ name += "#"+ node.id;
692
+ }
693
+
694
+ if (node.className) {
695
+ name += "."+ node.className;
696
+ }
697
+
698
+ return '<a href="" data-index="'+ index +
699
+ '" onclick="return false;" title="'+
700
+ Rte.i18n.Select +
701
+ '">'+ name +'</a>';
702
+
703
+ }).join(' &rsaquo; '));
704
+ },
705
+
706
+ /**
707
+ * Finds an element in the current status stack
708
+ *
709
+ * @param String tag name
710
+ * @param Object optional attributes
711
+ * @return raw element or null if nothing found
712
+ */
713
+ findElement: function(tag, attributes) {
714
+ if (tag) {
715
+ for (var i = this.nodes.length - 1, key, match; i > -1; i--) {
716
+ if (this.nodes[i].tagName === tag) {
717
+ match = true;
718
+
719
+ for (key in attributes) {
720
+ if (attributes[key] instanceof RegExp) {
721
+ match &= attributes[key].test(this.nodes[i].getAttribute(key));
722
+ } else {
723
+ match &= this.nodes[i].getAttribute(key) == attributes[key];
724
+ }
725
+ }
726
+
727
+ if (match) {
728
+ return this.nodes[i];
729
+ }
730
+ }
731
+ }
732
+ }
733
+
734
+ return null;
735
+ },
736
+
737
+ // protected
738
+
739
+ // runs the tools check
740
+ _checkTools: function() {
741
+ var tools = this.rte.tools, key;
742
+ for (key in tools) {
743
+ tools[key].check();
744
+ }
745
+ },
746
+
747
+ // finds the nodes from the current selection to the bottom
748
+ _findNodes: function() {
749
+ var node = this.rte.selection.element(),
750
+ editor = this.rte.editor._,
751
+ rte = this.rte._,
752
+ nodes = [],
753
+ tags = [];
754
+
755
+ this.nodes = [];
756
+ this.tags = [];
757
+
758
+ while (node && node !== rte) {
759
+ if (node.tagName) { // skipping the textual nodes
760
+ nodes.unshift(node);
761
+ tags.unshift(node.tagName);
762
+ }
763
+
764
+ node = node.parentNode;
765
+
766
+ if (node === editor) {
767
+ this.nodes = nodes;
768
+ this.tags = tags;
769
+ break;
770
+ }
771
+ }
772
+ },
773
+
774
+ // catches the mousedown on the links
775
+ _mousedown: function(event) {
776
+ var link = event.target;
777
+
778
+ if (link._.tagName === 'A') {
779
+ event.stop();
780
+ var index = link.get('data-index').toInt(),
781
+ node = this.nodes[index];
782
+
783
+ this.rte.selection.wrap(node);
784
+ }
785
+ }
786
+
787
+ });
788
+
789
+ /**
790
+ * Custom undo/redo manager
791
+ *
792
+ * The basic trouble is that the native undo manager
793
+ * dosn't support manual changes in the editable block
794
+ * plus it lacks an ability to save some things under IE
795
+ *
796
+ * So we manage our undo/redo states manually by whatching
797
+ * the 'change' event in the RTE instance
798
+ *
799
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
800
+ */
801
+ Rte.Undoer = new Class({
802
+
803
+ /**
804
+ * Basic constructor
805
+ *
806
+ * @param Rte rte
807
+ * @return void
808
+ */
809
+ initialize: function(rte) {
810
+ this.rte = rte;
811
+
812
+ function save() { this.undoer.save(); }
813
+ this.rte.on({ focus: save, change: save });
814
+
815
+ this.clear();
816
+ },
817
+
818
+ /**
819
+ * Clears up the undo history
820
+ *
821
+ * @return void
822
+ */
823
+ clear: function() {
824
+ this.stash = [];
825
+ this.index = -1;
826
+ },
827
+
828
+ /**
829
+ * Checks if there are undo steps
830
+ *
831
+ * @return boolean check result
832
+ */
833
+ hasUndo: function() {
834
+ return this.stash.length > 0 && this.index > 0;
835
+ },
836
+
837
+ /**
838
+ * Checks if there are redo steps
839
+ *
840
+ * @return boolean check result
841
+ */
842
+ hasRedo: function() {
843
+ return (this.stash.length - this.index) > 1;
844
+ },
845
+
846
+ /**
847
+ * Moves the history one step back
848
+ *
849
+ * @return void
850
+ */
851
+ undo: function() {
852
+ if (this.hasUndo()) {
853
+ this.set(-- this.index);
854
+ }
855
+ },
856
+
857
+ /**
858
+ * Moves the histor one step forward
859
+ *
860
+ * @return void
861
+ */
862
+ redo: function() {
863
+ if (this.hasRedo()) {
864
+ this.set(++ this.index);
865
+ }
866
+ },
867
+
868
+ /**
869
+ * Sets the history to the given step
870
+ *
871
+ * @param Integer step index
872
+ */
873
+ set: function(index) {
874
+ if (this.stash[this.index]) {
875
+ this.rte.editor.update(this.stash[this.index]);
876
+ this.rte.selection.restore();
877
+ }
878
+ },
879
+
880
+ /**
881
+ * Saves the current state of the editor
882
+ *
883
+ * @param Event the RTE's 'change' event with the 'tool' reference
884
+ * @return void
885
+ */
886
+ save: function(event) {
887
+ var tool = event && event.tool,
888
+ tools = this.rte.tools,
889
+ html, html1, html2;
890
+
891
+ if (!tool || (tool !== tools.Undo && tool !== tools.Redo)) {
892
+ this.rte.selection.store();
893
+
894
+ html = this.rte.editor._.innerHTML;
895
+
896
+ // stripping off the selection markers
897
+ html1 = html
898
+ .replace(SELECTION_START_RE, '')
899
+ .replace(SELECTION_END_RE, '');
900
+
901
+ html2 = (this.stash[this.index]||'')
902
+ .replace(SELECTION_START_RE, '')
903
+ .replace(SELECTION_END_RE, '');
904
+
905
+ if (html1 !== html2) {
906
+ // cutting loose possible redo steps
907
+ this.stash.length = this.index + 1;
908
+ this.stash.push(html);
909
+ this.index = this.stash.length - 1;
910
+
911
+ tools.Undo.check();
912
+ tools.Redo.check();
913
+ }
914
+
915
+ this.rte.selection.restore();
916
+ }
917
+ }
918
+
919
+ });
920
+
921
+ /**
922
+ * This class handles the selection ranges
923
+ *
924
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
925
+ */
926
+ Rte.Selection = new Class({
927
+
928
+ /**
929
+ * Basic constructor
930
+ *
931
+ * @param Rte rte
932
+ * @return void
933
+ */
934
+ initialize: function(rte) {
935
+ this.rte = rte;
936
+ },
937
+
938
+ /**
939
+ * gets/sets the current range object
940
+ *
941
+ * @param {Range} to set
942
+ * @return TextRange range
943
+ */
944
+ range: function(range) {
945
+ var selection = window.getSelection && window.getSelection();
946
+
947
+ if (range) {
948
+ if (selection) { // w3c
949
+ selection.removeAllRanges();
950
+ selection.addRange(range);
951
+ } else if (range._) { // IE
952
+ range._.select();
953
+ }
954
+
955
+ } else {
956
+ try {
957
+ range = selection.getRangeAt(0); // FF, Opera, IE9
958
+ } catch (e) {
959
+ try {
960
+ range = document.createRange(); // WebKit
961
+ } catch (ee) {
962
+ range = new IERangeEmulator(); // Old IE
963
+ }
964
+ }
965
+
966
+ return range;
967
+ }
968
+ },
969
+
970
+ /**
971
+ * Returns the dom-node that's currently in focus
972
+ *
973
+ * @return raw dom-element
974
+ */
975
+ element: function() {
976
+ var range = this.range(), node = range.commonAncestorContainer;
977
+
978
+ // if there is a selection, trying those
979
+ if (!range.collapsed) {
980
+ if (
981
+ range.startContainer === node &&
982
+ range.startOffset - range.endOffset < 2 &&
983
+ range.startContainer.hasChildNodes()
984
+ ) {
985
+ node = range.startContainer.childNodes[range.startOffset];
986
+ }
987
+ }
988
+
989
+ node = node && node.nodeType === 3 ? node.parentNode : node;
990
+
991
+ return node || null;
992
+ },
993
+
994
+ /**
995
+ * Puts current selection over the given raw dom-node
996
+ *
997
+ * @param raw dom-node
998
+ * @return void
999
+ */
1000
+ wrap: function(node) {
1001
+ var range = this.range();
1002
+ range.selectNode(node);
1003
+ this.range(range);
1004
+ },
1005
+
1006
+ /**
1007
+ * Returns the selection text
1008
+ *
1009
+ * @return String selection text
1010
+ */
1011
+ text: function() {
1012
+ return this.range().toString();
1013
+ },
1014
+
1015
+ /**
1016
+ * Cheks if the selection is empty
1017
+ *
1018
+ * @return boolean check result
1019
+ */
1020
+ empty: function() {
1021
+ return this.text() === '';
1022
+ },
1023
+
1024
+ /**
1025
+ * Returns the HTML content of the selection
1026
+ *
1027
+ * @return String html content
1028
+ */
1029
+ html: function() {
1030
+ var range = this.range(), tmp, fragment;
1031
+
1032
+ if (range._) {
1033
+ return range._.htmlText;
1034
+ } else {
1035
+ tmp = document.createElement('div');
1036
+ fragment = range.cloneContents();
1037
+
1038
+ while (fragment.firstChild) {
1039
+ tmp.appendChild(fragment.firstChild);
1040
+ }
1041
+
1042
+ return tmp.innerHTML;
1043
+ }
1044
+ },
1045
+
1046
+ /**
1047
+ * executes a command on this editing area
1048
+ *
1049
+ * @param String command name
1050
+ * @param mixed command value
1051
+ * @return void
1052
+ */
1053
+ exec: function(command, value) {
1054
+ try {
1055
+ // it throws errors in some cases in the non-design mode
1056
+ document.execCommand(command, false, value);
1057
+ } catch(e) {
1058
+ // emulating insert html under IE
1059
+ if (command === 'inserthtml') {
1060
+ this.range()._.pasteHTML(value);
1061
+ }
1062
+ }
1063
+ },
1064
+
1065
+ /**
1066
+ * Saves the selection by inserting special SPAN elements
1067
+ * in places where the selection starts and ends so that
1068
+ * it could be restored later, even if the editor innerHTML
1069
+ * property was manipulated directly
1070
+ *
1071
+ * @return {Rte.Selection} this
1072
+ */
1073
+ store: function() {
1074
+ var range = this.range();
1075
+
1076
+ // reclonning the data so it wasn't lost on changes
1077
+ range = {
1078
+ startContainer: range.startContainer,
1079
+ startOffset: range.startOffset,
1080
+ endContainer: range.endContainer,
1081
+ endOffset: range.endOffset,
1082
+ collapsed: range.collapsed
1083
+ };
1084
+
1085
+ /**
1086
+ * Places the selection markers into the editor's structure
1087
+ *
1088
+ * @param String name 'end' or 'start'
1089
+ * @return void
1090
+ */
1091
+ function place_marker(name) {
1092
+ var node = range[name + 'Container'],
1093
+ offset = range[name + 'Offset'],
1094
+ marker = document.createElement('span'),
1095
+ parent = node.parentNode,
1096
+ text = node.nodeValue,
1097
+ ending = document.createTextNode((''+text).substr(offset));
1098
+
1099
+ marker.setAttribute('rrte-'+name, '1');
1100
+
1101
+ function insert_after(content, anchor) {
1102
+ if (anchor.nextSibling) {
1103
+ anchor.parentNode.insertBefore(content, anchor.nextSibling);
1104
+ } else {
1105
+ anchor.parentNode.appendChild(content);
1106
+ }
1107
+ }
1108
+
1109
+ if (node.nodeType === 3) { // text-node
1110
+ if (offset === 0) {
1111
+ parent.insertBefore(marker,
1112
+ // in case both of the markers are at the beginning of
1113
+ // the same node, the 'end' marker will be already there
1114
+ // and we will need to insert the 'start' one before it.
1115
+ name === 'start' && range.collapsed ? node.previousSibling : node
1116
+ );
1117
+ } else if (offset === text.length) {
1118
+ insert_after(marker, node);
1119
+ } else { // splitting the text node in two
1120
+ node.nodeValue = text.substr(0, offset);
1121
+ insert_after(ending, node);
1122
+ parent.insertBefore(marker, ending);
1123
+ }
1124
+
1125
+ } else if (node.nodeType === 1) { // elements
1126
+ if (offset === 0) {
1127
+ if (node.firstChild) {
1128
+ node.insertBefore(marker, node.firstChild);
1129
+ } else if (node.hasChildNodes()) {
1130
+ node.appendChild(marker);
1131
+ }
1132
+ } else if (offset === node.childNodes.length) {
1133
+ node.appendChild(marker);
1134
+ } else {
1135
+ node.insertBefore(marker, node.childNodes[offset]);
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ // NOTE: the 'end' should be before the 'start' in case both of them
1141
+ // are in the same node, so that the offest didn't shift after
1142
+ // we insert the marker nodes
1143
+ place_marker('end');
1144
+ place_marker('start');
1145
+ },
1146
+
1147
+ /**
1148
+ * Restores the selection by previously placed
1149
+ * special SPAN elements and removes them afterwards
1150
+ *
1151
+ * @return {Rte.Selection} this
1152
+ */
1153
+ restore: function() {
1154
+ var elements = $A(this.rte.editor._.getElementsByTagName('span')),
1155
+ i=0, method, parent, offset, range = this.range();
1156
+
1157
+ for (; i < elements.length; i++) {
1158
+ method = elements[i].getAttribute('rrte-start') ? 'setStart' :
1159
+ elements[i].getAttribute('rrte-end') ? 'setEnd' : false;
1160
+
1161
+ if (method) {
1162
+ parent = elements[i].parentNode;
1163
+
1164
+ if (range._) {
1165
+ range[method](elements[i]);
1166
+ } else {
1167
+ offset = IER_getIndex(elements[i]);
1168
+ range[method](parent, offset);
1169
+ }
1170
+
1171
+ parent.removeChild(elements[i]);
1172
+ }
1173
+ }
1174
+
1175
+ this.range(range);
1176
+ }
1177
+ });
1178
+
1179
+ var
1180
+
1181
+ SELECTION_START_MARKER = '<span rrte-start="1"></span>',
1182
+ SELECTION_END_MARKER = '<span rrte-end="1"></span>',
1183
+ SELECTION_START_RE = new RegExp(RegExp.escape(SELECTION_START_MARKER), 'i'),
1184
+ SELECTION_END_RE = new RegExp(RegExp.escape(SELECTION_END_MARKER), 'i');
1185
+
1186
+
1187
+ /**
1188
+ * W3C ranges API emulator for the old IE browsers
1189
+ *
1190
+ * Based on the `InternetExplorerRange` implementation
1191
+ * from the http://mozile.mozdev.org project
1192
+ * by James A. Overton <james@overton.ca>
1193
+ *
1194
+ * Originally licensed under MPL/GPL2/LGPL2 licenses
1195
+ *
1196
+ * Copyrgiht (C) 2011 Nikolay Nemshilov
1197
+ */
1198
+ var IERangeEmulator = new Class({
1199
+
1200
+ // standard w3c attributes
1201
+ collapsed: null,
1202
+ startContainer: null,
1203
+ startOffset: null,
1204
+ endContainer: null,
1205
+ endOffset: null,
1206
+ commonAncestorContainer: null,
1207
+
1208
+ /**
1209
+ * Basic constructor
1210
+ *
1211
+ * @return void
1212
+ */
1213
+ initialize: function() {
1214
+ this._ = document.selection.createRange();
1215
+
1216
+ if (document.selection.type === 'Control') {
1217
+ this.startContainer = this.endContainer = this.commonAncestorContainer = this._(0);
1218
+ this.startOffset = this.endOffset = 0;
1219
+ } else {
1220
+ //startPoint
1221
+ var range = this._.duplicate();
1222
+ range.collapse(true);
1223
+ range = IER_getPosition(range);
1224
+
1225
+ this.startContainer = range.node;
1226
+ this.startOffset = range.offset;
1227
+
1228
+ //endPoint
1229
+ range = this._.duplicate();
1230
+ range.collapse(false);
1231
+ range = IER_getPosition(range);
1232
+
1233
+ this.endContainer = range.node;
1234
+ this.endOffset = range.offset;
1235
+
1236
+ // the rest of the properties
1237
+ IER_commonAncestorContainer(this);
1238
+ }
1239
+
1240
+ IER_collapsed(this);
1241
+ },
1242
+
1243
+ /**
1244
+ * Sets the starting point for the range
1245
+ *
1246
+ * @param {Node} node
1247
+ * @param {Number} offset
1248
+ * @return void
1249
+ */
1250
+ setStart: function(node, offset) {
1251
+ var range = this._.duplicate();
1252
+
1253
+ range.moveToElementText(node);
1254
+ range.collapse(true);
1255
+
1256
+ this._.setEndPoint('StartToStart', range);
1257
+
1258
+ this.startContainer = node;
1259
+ this.startOffset = offset;
1260
+
1261
+ if (this.endContainer === null && this.endOffset === null) {
1262
+ this.endContainer = node;
1263
+ this.endOffset = offset;
1264
+ }
1265
+
1266
+ IER_commonAncestorContainer(this);
1267
+ IER_collapsed(this);
1268
+ },
1269
+
1270
+ /**
1271
+ * Setting the end point for the range
1272
+ *
1273
+ * @param {Node} node
1274
+ * @param {Number} offset
1275
+ * @return void
1276
+ */
1277
+ setEnd: function (node, offset) {
1278
+ var range = this._.duplicate();
1279
+
1280
+ range.moveToElementText(node);
1281
+ range.collapse(true);
1282
+
1283
+ this._.setEndPoint('EndToEnd', range);
1284
+
1285
+ this.endContainer = node;
1286
+ this.endOffset = offset;
1287
+
1288
+ if (this.startContainer === null && this.startOffset === null) {
1289
+ this.startContainer = node;
1290
+ this.startOffset = offset;
1291
+ }
1292
+
1293
+ IER_commonAncestorContainer(this);
1294
+ IER_collapsed(this);
1295
+ },
1296
+
1297
+ /**
1298
+ * Wrapps the range around the given node
1299
+ *
1300
+ * @param {Node} node
1301
+ * @return void
1302
+ */
1303
+ selectNode: function (node) {
1304
+ this._.moveToElementText(node);
1305
+ },
1306
+
1307
+ /**
1308
+ * Selection text emulation
1309
+ *
1310
+ * @return {String} text
1311
+ */
1312
+ toString: function() {
1313
+ return ''+ this._.text;
1314
+ }
1315
+ });
1316
+
1317
+
1318
+ //////////////////////////////////////////////////////////////////////////////
1319
+ // Some private methods for the IERangeEmaulator
1320
+ //////////////////////////////////////////////////////////////////////////////
1321
+
1322
+
1323
+ /**
1324
+ * Finds the standard node/offset pair out of the
1325
+ * IE range object
1326
+ *
1327
+ * @param {TextRange} ie text range object
1328
+ * @return {Object} 'node' and 'offset' pairs
1329
+ */
1330
+ function IER_getPosition(original_range) {
1331
+ var element = original_range.parentElement(),
1332
+ range, range_size, direction, node, node_size;
1333
+
1334
+ range = document.body.createTextRange();
1335
+ range.moveToElementText(element);
1336
+ range.setEndPoint("EndToStart", original_range);
1337
+
1338
+ range_size = range.text.length;
1339
+
1340
+ // Choose Direction
1341
+ if (range_size < element.innerText.length / 2) {
1342
+ direction = 1;
1343
+ node = element.firstChild;
1344
+ } else {
1345
+ direction = -1;
1346
+ node = element.lastChild;
1347
+ range.moveToElementText(element);
1348
+ range.setEndPoint("StartToStart", original_range);
1349
+ range_size = range.text.length;
1350
+ }
1351
+
1352
+ // Loop through child nodes
1353
+ while (node) {
1354
+ switch (node.nodeType) {
1355
+ case 3: // text-node
1356
+ node_size = node.data.length;
1357
+ if(node_size < range_size) {
1358
+ range_size -= node_size;
1359
+
1360
+ if (direction === 1) {
1361
+ range.moveStart("character", range_size);
1362
+ } else {
1363
+ range.moveEnd("character", -range_size);
1364
+ }
1365
+
1366
+ } else {
1367
+ return direction === 1 ?
1368
+ {node: node, offset: range_size} :
1369
+ {node: node, offset: node_size - range_size};
1370
+ }
1371
+ break;
1372
+
1373
+ case 1: // element-node
1374
+ node_size = node.innerText.length;
1375
+
1376
+ if (direction === 1) {
1377
+ range.moveStart("character", node_size);
1378
+ } else {
1379
+ range.moveEnd("character", -node_size);
1380
+ }
1381
+
1382
+ range_size = range_size - node_size;
1383
+ break;
1384
+ }
1385
+
1386
+ node = direction === 1 ? node.nextSibling : node.previousSibling;
1387
+ }
1388
+
1389
+ // The TextRange was not found. Return a reasonable value instead.
1390
+ return {node: element, offset: 0};
1391
+ }
1392
+
1393
+ /**
1394
+ * Assigns a suitable `commonAncestorContainer` property for the range
1395
+ *
1396
+ * @param {IERangeEmulator} range
1397
+ * @return void
1398
+ */
1399
+ function IER_commonAncestorContainer(range) {
1400
+ range.commonAncestorContainer = range._.parentElement();
1401
+ }
1402
+
1403
+ /**
1404
+ * Sets the `collapsed` property for the range
1405
+ *
1406
+ * @param {IERangeEmulator} range
1407
+ * return void
1408
+ */
1409
+ function IER_collapsed(range) {
1410
+ range.collapsed =
1411
+ range.startContainer === range.endContainer &&
1412
+ range.startOffset === range.endOffset;
1413
+ }
1414
+
1415
+ /**
1416
+ * Finds out the node's index among it's siblings
1417
+ *
1418
+ * @param {Node} node
1419
+ * @return {Number} index
1420
+ */
1421
+ function IER_getIndex(node) {
1422
+ var index = 0;
1423
+
1424
+ while ((node = node.previousSibling)) {
1425
+ index ++;
1426
+ }
1427
+
1428
+ return index;
1429
+ }
1430
+
1431
+
1432
+ /**
1433
+ * The basic tools class
1434
+ *
1435
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
1436
+ */
1437
+ Rte.Tool = new Class(Element, {
1438
+
1439
+ block: true, // should the 'keypress' event be blocked
1440
+ blip: false, // whether it should be highlighted when used
1441
+ changes: true, // if this tool should fire 'change' on the rte
1442
+
1443
+ shortuct: null, // the shortuct key-code
1444
+ shiftKey: false, // if it should trigger with a shift-key only
1445
+
1446
+ /**
1447
+ * Basic constructor
1448
+ *
1449
+ * @param Rte rte reference
1450
+ * @return Rte.Tool this
1451
+ */
1452
+ initialize: function(rte) {
1453
+ var name;
1454
+
1455
+ // searching for the tool-name
1456
+ for (name in Rte.Tools) {
1457
+ if (Rte.Tools[name] === this.constructor) { break; }
1458
+ }
1459
+
1460
+ this.name = name;
1461
+ this.shortcut = this.shortcut || Rte.Shortcuts[name];
1462
+
1463
+ this.$super('div', {
1464
+ 'html': '<div class="icon"></div>', // <- icon container
1465
+ 'class': 'tool '+ name.toLowerCase(),
1466
+ 'title': (Rte.i18n[name] || name) + (
1467
+ this.shortcut ? " ("+ this.shortcut + ")" : ""
1468
+ )
1469
+ });
1470
+
1471
+ // registering the tool
1472
+ this.rte = rte;
1473
+ rte.tools[name] = this;
1474
+
1475
+ // hooking up the shortcuts
1476
+ this.shortcut = this.shortcut && this.shortcut.toLowerCase();
1477
+ this.shiftKey = this.shortcut && this.shortcut.indexOf('shift') > -1;
1478
+ this.shortcut = this.shortcut && this.shortcut.toUpperCase().charCodeAt(this.shortcut.length - 1);
1479
+
1480
+ // connecting the mousedown the way that the editor din't loose the focus
1481
+ this.onMousedown(function(e) {
1482
+ e.stop(); this.mousedown();
1483
+ });
1484
+
1485
+ // allowing some nice chains in the subclass
1486
+ return this;
1487
+ },
1488
+
1489
+ // those three methods should be implemented in subclasses
1490
+ exec: function() { }, /// the actual process
1491
+ active: function() { return false; }, // all tools not active by default
1492
+ enabled: function() { return true; }, // all tools enabled by default
1493
+
1494
+ /**
1495
+ * The entry point for all the tools, if you need to call a tool,
1496
+ * call this method. __DON'T CALL__ the #exec method directly!
1497
+ *
1498
+ * @param {Event} 'keydown' event
1499
+ * @return void
1500
+ */
1501
+ call: function(event) {
1502
+ if (!this.disabled) {
1503
+ if (event && this.block) { event.stop(); }
1504
+
1505
+ this.exec();
1506
+ this.rte.status.update();
1507
+ this.rte.fire('change', {tool: this});
1508
+
1509
+ if (this.blip) { this.highlight(); }
1510
+ }
1511
+ },
1512
+
1513
+ /**
1514
+ * Checks the command's status
1515
+ *
1516
+ * @return void
1517
+ */
1518
+ check: function() {
1519
+ this._.className = this._.className.replace(' disabled', '');
1520
+ this.disabled = false;
1521
+
1522
+ if ((this.name === 'Source' || this.rte.srcMode !== true) && this.enabled()) {
1523
+ this._.className = this._.className.replace(' active', '');
1524
+ if (this.active()) {
1525
+ this._.className += ' active';
1526
+ }
1527
+
1528
+ } else {
1529
+ this._.className += ' disabled';
1530
+ this.disabled = true;
1531
+ }
1532
+ },
1533
+
1534
+ /**
1535
+ * Replacing the highlight method with some css stuff instead of an Fx
1536
+ *
1537
+ * @return Rte.Tool this
1538
+ */
1539
+ highlight: function() {
1540
+ R(this.addClass('highlight').removeClass).bind(this, 'highlight').delay(100);
1541
+ },
1542
+
1543
+ // protected
1544
+
1545
+ // mousedown event receiver (might be replaced in subclasses)
1546
+ mousedown: function() {
1547
+ this.call();
1548
+ }
1549
+
1550
+ });
1551
+
1552
+ /**
1553
+ * Native command related abstract tool
1554
+ *
1555
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
1556
+ */
1557
+ Rte.Tool.Command = new Class(Rte.Tool, {
1558
+ command: null, // execCommand name
1559
+ value: null, // execCommand value
1560
+
1561
+ /**
1562
+ * calling the command exec
1563
+ *
1564
+ * @return void
1565
+ */
1566
+ exec: function() {
1567
+ this.rte.selection.exec(this.command, this.value);
1568
+ },
1569
+
1570
+ /**
1571
+ * Checks if the command is enabled at all
1572
+ *
1573
+ * @return boolean check result
1574
+ */
1575
+ enabled: function() {
1576
+ try {
1577
+ return document.queryCommandEnabled(this.command);
1578
+ } catch(e) {
1579
+ return false;
1580
+ }
1581
+ },
1582
+
1583
+ /**
1584
+ * Queries if the command is in active state
1585
+ *
1586
+ * @return boolean check result
1587
+ */
1588
+ active: function() {
1589
+ try { // copy/paste commands cause errors under FF by default
1590
+ return this.value === null ?
1591
+ document.queryCommandState(this.command) :
1592
+ document.queryCommandValue(this.command) == this.value;
1593
+ } catch(e) {
1594
+ return false;
1595
+ }
1596
+ }
1597
+ });
1598
+
1599
+ /**
1600
+ * Text formatting specific abstract tool
1601
+ *
1602
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
1603
+ */
1604
+ Rte.Tool.Format = new Class(Rte.Tool, {
1605
+ tag: null, // element's tag name
1606
+ atts: {}, // element's attributes
1607
+
1608
+ /**
1609
+ * Formatting tools basic constructor
1610
+ *
1611
+ * @param Rte rte
1612
+ * @return Rte.ToolFormat this
1613
+ */
1614
+ initialize: function(rte) {
1615
+ this.$super(rte);
1616
+ this.tag = (this.tag || Rte.Tags[this.name] || '').toUpperCase();
1617
+ return this;
1618
+ },
1619
+
1620
+ /**
1621
+ * triggering the formatting
1622
+ *
1623
+ * @return void
1624
+ */
1625
+ exec: function() {
1626
+ this[this.active() ? 'unformat' : 'format']();
1627
+ },
1628
+
1629
+ /**
1630
+ * Overloading the activity checks
1631
+ *
1632
+ * @return boolean check result
1633
+ */
1634
+ active: function() {
1635
+ return this.element() !== null;
1636
+ },
1637
+
1638
+ // protected
1639
+
1640
+ /**
1641
+ * Tries to find a currently active element
1642
+ *
1643
+ * @return raw dom element or null
1644
+ */
1645
+ element: function() {
1646
+ return this.rte.status.findElement(this.tag, this.attrs);
1647
+ },
1648
+
1649
+ /**
1650
+ * Removes the formatting
1651
+ *
1652
+ * @return void
1653
+ */
1654
+ unformat: function() {
1655
+ this._format(false);
1656
+ },
1657
+
1658
+ /**
1659
+ * Formats the block according to the settings
1660
+ *
1661
+ * @return void
1662
+ */
1663
+ format: function() {
1664
+ this._format(true);
1665
+ },
1666
+
1667
+ // private
1668
+
1669
+ /**
1670
+ * The actual formatting/unformatting process mumbo-jumbo
1671
+ *
1672
+ * @param boolean formatting direction true/false
1673
+ * @return void
1674
+ */
1675
+ _format: function(formatting) {
1676
+ var open_tag = '<'+ this.tag,
1677
+ close_tag = '</'+ this.tag + '>',
1678
+ editor = this.rte.editor,
1679
+ selection = this.rte.selection,
1680
+ range = selection.range(),
1681
+ selected = selection.text(),
1682
+ element = this.element(),
1683
+ content = element && (element.textContent || element.innerText);
1684
+
1685
+ // building the open-tag attributes
1686
+ for (var attr in this.attrs) {
1687
+ open_tag += ' '+ attr +'="'+ this.attrs[attr]+ '"';
1688
+ }
1689
+ open_tag += ">";
1690
+
1691
+ selection.store();
1692
+
1693
+ // Old IEs screw with the starting position
1694
+ // placing it before the open tag, so here we switch it back
1695
+ if (!formatting && range._) {
1696
+ editor.html(editor.html().replace(
1697
+ new RegExp(RegExp.escape(SELECTION_START_MARKER + open_tag), 'i'),
1698
+ open_tag + SELECTION_START_MARKER
1699
+ ));
1700
+ }
1701
+
1702
+
1703
+ if (formatting) {
1704
+ editor.html(editor.html()
1705
+ .replace(SELECTION_START_RE, open_tag + SELECTION_START_MARKER)
1706
+ .replace(SELECTION_END_RE, SELECTION_END_MARKER + close_tag)
1707
+ );
1708
+ } else if (element && selected === content) {
1709
+ // plainly remove the element if it was fully selected
1710
+ // in case there are several nested elements so that
1711
+ // we didn't get screwed with the regexps manipulations
1712
+ editor.removeElement(element);
1713
+ } else {
1714
+ editor.html(editor.html()
1715
+ .replace(SELECTION_START_RE, close_tag + SELECTION_START_MARKER)
1716
+ .replace(SELECTION_END_RE, SELECTION_END_MARKER + open_tag)
1717
+
1718
+ // cleaning up empty tags that might left
1719
+ .replace(/<([a-z]+)[^>]*?>\s*?<\/\1>/ig, '')
1720
+ );
1721
+ }
1722
+
1723
+ selection.restore();
1724
+ }
1725
+
1726
+ });
1727
+
1728
+ /**
1729
+ * A _shared module_ to provide the `options` list functionality
1730
+ * for the tools
1731
+ *
1732
+ * Copyright (C) 2010 Nikolay Nemshilov
1733
+ */
1734
+ Rte.Tool.Options = {
1735
+
1736
+ /**
1737
+ * Builds the options list
1738
+ *
1739
+ * @param Object key -> value hash
1740
+ */
1741
+ build: function(options) {
1742
+ this.trigger = $E('div', {'class': 'trigger', 'html': '&middot;'});
1743
+ this.display = $E('div', {'class': 'display'});
1744
+ this.options = $E('ul', {'class': 'options'});
1745
+
1746
+ this
1747
+ .addClass('with-options')
1748
+ .append(this.display, this.options)
1749
+ .insert(this.trigger, 'top');
1750
+
1751
+ this.items = {};
1752
+
1753
+ for (var value in options) {
1754
+ this.items[value] = $E('li').insert(options[value]);
1755
+ this.items[value].insertTo(this.options).value = value;
1756
+ }
1757
+
1758
+ // creating an the reset value
1759
+ this.items[''] = $E('li', {'class': 'remove', 'html': '--', 'title': Rte.i18n.Remove});
1760
+ this.items[''].insertTo(this.options, 'top').value = '';
1761
+
1762
+ // catching the clicks
1763
+ this.options.onMousedown(R(this.pick).bind(this));
1764
+
1765
+ // hidding the menu when the user interacts with the document outside of the document
1766
+ var hide = R(this.options.hide).bind(this.options, null);
1767
+ $(document).on({mousedown: hide,
1768
+ keydown: function(event) {
1769
+ if (event.keyCode === 27) { hide(); }
1770
+ }
1771
+ });
1772
+
1773
+ this.value = '';
1774
+ this.updateDisplay(null);
1775
+
1776
+ return this;
1777
+ },
1778
+
1779
+ // protected
1780
+
1781
+ // handling an option pick
1782
+ pick: function(event) {
1783
+ var target = event.stop().target;
1784
+
1785
+ if (target._.tagName !== 'LI') {
1786
+ target = target.parent('LI');
1787
+ }
1788
+
1789
+ if (target.value !== undefined) {
1790
+ this.options.hide();
1791
+ this.value = target.value;
1792
+ this.updateDisplay(this.value || null);
1793
+ this.markActive();
1794
+ this.exec();
1795
+ }
1796
+ },
1797
+
1798
+ // toggling the menu on the icon-click
1799
+ mousedown: function() {
1800
+ if (!this.disabled) {
1801
+ $$('.rui-rte-toolbar div.with-options ul.options')
1802
+ .without(this.options).each('hide');
1803
+
1804
+ // marking the current value so it could be seen
1805
+ if (this.options.hidden() && this.value !== null) {
1806
+ this.markActive();
1807
+ }
1808
+
1809
+ this.options.toggle('fade', {duration: 'short'});
1810
+ }
1811
+ },
1812
+
1813
+ // marks the currently active item
1814
+ markActive: function() {
1815
+ for (var item in this.items) {
1816
+ this.items[item][
1817
+ item === this.value ? 'addClass' : 'removeClass'
1818
+ ]('active');
1819
+ }
1820
+ },
1821
+
1822
+ // updates the display
1823
+ updateDisplay: function(value) {
1824
+ this.display.update(
1825
+ value !== null && value in this.items ?
1826
+ this.items[value].text() : this._.title
1827
+ );
1828
+ }
1829
+
1830
+ };
1831
+
1832
+ /**
1833
+ * An abstract formatting tool that works with styles
1834
+ * basically it wraps a selection with a 'span' tag and then
1835
+ * handles the 'style' attribute, adds/removes various styles, etc.
1836
+ *
1837
+ * Copyright (C) 2010 Nikolay Nemshilov
1838
+ */
1839
+ Rte.Tool.Style = new Class(Rte.Tool.Format, {
1840
+ include: Rte.Tool.Options,
1841
+
1842
+ tag: 'span', // tag name of the element to be used
1843
+ style: null, // the style property name (dashed)
1844
+
1845
+ /**
1846
+ * Joint constructor
1847
+ *
1848
+ * @param Rte rte
1849
+ * @return Rte.Tool.StyleOptions
1850
+ */
1851
+ initialize: function(rte, options) {
1852
+ // a regular expression to process the `style` property
1853
+ this.re = new RegExp("(^|;)\\s*"+ RegExp.escape(this.style + ":")+ "\\s*(.+?)\\s*(;|$)");
1854
+ this.attrs = { style: this.re };
1855
+
1856
+ this.$super(rte).build(options);
1857
+
1858
+ return this;
1859
+ },
1860
+
1861
+ /**
1862
+ * Triggers the action
1863
+ *
1864
+ * @return void
1865
+ */
1866
+ exec: function() {
1867
+ // unformatting if there is a value
1868
+ if (this.active()) {
1869
+ this.attrs = {style: this.style + ":" + this._value};
1870
+ this.unformat();
1871
+ }
1872
+
1873
+ // formatting if some value was asked
1874
+ if (this.value) {
1875
+ this.attrs = {style: this.style + ":" + this.value};
1876
+ this.format();
1877
+ }
1878
+
1879
+ // getting back the RE checker
1880
+ this.attrs = {style: this.re};
1881
+ },
1882
+
1883
+ /**
1884
+ * Overloading the original method so that we could do some additional
1885
+ * features with the actual current value
1886
+ *
1887
+ * @return void
1888
+ */
1889
+ active: function() {
1890
+ var element = this.element(), active = false, value = null;
1891
+
1892
+ if (element !== null) {
1893
+ this._value = value = this.getStyleValue();
1894
+ active = true;
1895
+ }
1896
+
1897
+ this.updateDisplay(value);
1898
+
1899
+ return active;
1900
+ },
1901
+
1902
+ // protected
1903
+
1904
+ /**
1905
+ * Finds the current style value (if any)
1906
+ *
1907
+ * @return string style value or null if nothing
1908
+ */
1909
+ getStyleValue: function() {
1910
+ var element = this.element();
1911
+ var attribute = element !== null ? element.getAttribute('style') : null;
1912
+
1913
+ if (attribute !== null) {
1914
+ if ((attribute = attribute.match(this.re)) !== null) {
1915
+ attribute = attribute[2];
1916
+ }
1917
+ }
1918
+
1919
+ return attribute;
1920
+ }
1921
+ });
1922
+
1923
+ /**
1924
+ * an abstract color-picking tool
1925
+ *
1926
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
1927
+ */
1928
+ Rte.Tool.Color = new Class(Rte.Tool.Style, {
1929
+
1930
+ extend: {
1931
+ COLORS: R([
1932
+ // TODO that's ain't no cool hacker's approach!
1933
+ '000000 444444 666666 999999 cccccc eeeeee f4f4f4 ffffff',
1934
+ 'f24020 f79c33 fff84c 6af244 5ef9fd 0048f7 8950f7 ee5ff8',
1935
+ 'e39e9b f5cba1 fee3a1 bcd3ab a6c3c8 a2c6e5 b1abd3 d0aabc '+
1936
+ 'd77169 f1b374 fdd675 9cbe83 7ca4ae 74aad8 8983bf bb839f '+
1937
+ 'cc0100 e79138 f1c332 69a84f 45818e 3d85c6 674ea7 a64d79 '+
1938
+ '990000 b45f05 bf9000 38761c 134f5c 0b5394 351b75 751a47 '+
1939
+ '660000 783e03 7f6000 264e13 0b333d 063763 1f124c 4c1030'
1940
+ ])
1941
+ },
1942
+
1943
+ /**
1944
+ * Basic constructor, builds the colors picking panel
1945
+ *
1946
+ * @param Rte rte
1947
+ * @return Rte.Tool.Color this
1948
+ */
1949
+ initialize: function(rte) {
1950
+ this.$super(rte, {}).addClass('color');
1951
+ this.display.clean();
1952
+
1953
+ // building the color picker menu
1954
+ Rte.Tool.Color.COLORS.each(function(line) {
1955
+ var group = $E('li', {'class': 'group'}),
1956
+ list = $E('ul').insertTo(group),
1957
+ colors = line.split(' '), i = 0, color, forecolor;
1958
+
1959
+ for (; i < colors.length; i++) {
1960
+ color = '#' + colors[i];
1961
+ forecolor = (parseInt('ffffff', 16) - parseInt(colors[i], 16)).toString(16);
1962
+
1963
+ // IE will get screwed if the length of the color is less than 6
1964
+ while (forecolor.length < 6) {
1965
+ forecolor += '0';
1966
+ }
1967
+
1968
+ this.items[color] = $E('li', {
1969
+ html: '&bull;',
1970
+ style: {
1971
+ background: color,
1972
+ color: '#'+ forecolor
1973
+ }
1974
+ });
1975
+
1976
+ this.items[color].insertTo(list).value = color;
1977
+ }
1978
+
1979
+ this.options.append(group);
1980
+ }, this);
1981
+
1982
+ return this;
1983
+ },
1984
+
1985
+ // protected
1986
+
1987
+ /**
1988
+ * Overloading the property so that it converted any color formats
1989
+ * into a six chars hex value
1990
+ *
1991
+ * @return String current color or null
1992
+ */
1993
+ getStyleValue: function() {
1994
+ var color = this.$super(), match;
1995
+
1996
+ if (color !== null) {
1997
+ if ((match = /^#(\w)(\w)(\w)$/.exec(color))) {
1998
+ color = "#"+ match[1]+match[1]+match[2]+match[2]+match[3]+match[3];
1999
+ } else if ((match = /^\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)\s*$/.exec(color))) {
2000
+ color = "#"+ R(match.slice(1)).map(function(bit) {
2001
+ bit = (bit-0).toString(16);
2002
+ return bit.length === 1 ? '0'+bit : bit;
2003
+ }).join('');
2004
+ }
2005
+ }
2006
+
2007
+ return color;
2008
+ },
2009
+
2010
+ /**
2011
+ * Replacing the original method so that
2012
+ * it changed the color of the display thing
2013
+ * instead of text
2014
+ *
2015
+ * @param String value
2016
+ * @return void
2017
+ */
2018
+ updateDisplay: function(value) {
2019
+ this.display._.style.background = value === null ?
2020
+ 'transparent' : value;
2021
+ }
2022
+ });
2023
+
2024
+ /**
2025
+ * An abstract URL tool, used with the links, images, videos, etc.
2026
+ *
2027
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2028
+ */
2029
+ Rte.Tool.Url = new Class(Rte.Tool, {
2030
+ attr: null, // the url-attribute 'src', 'href', etc.
2031
+
2032
+ exec: function(url) {
2033
+ if (url === undefined) {
2034
+ this.prompt();
2035
+ } else {
2036
+ if (url) {
2037
+ this[this.element() ? 'url' : 'create'](url);
2038
+ } else {
2039
+ this.rte.editor.removeElement(this.element());
2040
+ }
2041
+ }
2042
+ },
2043
+
2044
+ active: function() {
2045
+ return this.element() !== null;
2046
+ },
2047
+
2048
+ prompt: function() {
2049
+ var url = prompt(Rte.i18n.UrlAddress, this.url() || 'http://some.url.com');
2050
+
2051
+ if (url !== null) {
2052
+ this.exec(url);
2053
+ }
2054
+ },
2055
+
2056
+ // protected
2057
+
2058
+ url: function(url) {
2059
+ if (this.element()) {
2060
+ if (url !== undefined) {
2061
+ this.element()[this.attr] = url;
2062
+ } else {
2063
+ return this.element()[this.attr];
2064
+ }
2065
+ }
2066
+ },
2067
+
2068
+ create: function(url) {
2069
+ this.rte.selection.exec(this.command, url);
2070
+ }
2071
+ });
2072
+
2073
+ /**
2074
+ * Making the things bold tool
2075
+ *
2076
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2077
+ */
2078
+ Rte.Tools.Bold = new Class(Rte.Tool.Format, {
2079
+ });
2080
+
2081
+ /**
2082
+ * Making the things italic
2083
+ *
2084
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2085
+ */
2086
+ Rte.Tools.Italic = new Class(Rte.Tool.Format, {
2087
+ });
2088
+
2089
+ /**
2090
+ * Making the things underline tool
2091
+ *
2092
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2093
+ */
2094
+ Rte.Tools.Underline = new Class(Rte.Tool.Format, {
2095
+ });
2096
+
2097
+ /**
2098
+ * Making the things strike through
2099
+ *
2100
+ * Copyright (C) 2010 Nikolay Nemshilov
2101
+ */
2102
+ Rte.Tools.Strike = new Class(Rte.Tool.Format, {
2103
+ });
2104
+
2105
+ /**
2106
+ * The cut tool
2107
+ *
2108
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2109
+ */
2110
+ Rte.Tools.Cut = new Class(Rte.Tool.Command, {
2111
+ command: 'cut',
2112
+ block: false,
2113
+ blip: true
2114
+ });
2115
+
2116
+ /**
2117
+ * The copy tool
2118
+ *
2119
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2120
+ */
2121
+ Rte.Tools.Copy = new Class(Rte.Tools.Cut, {
2122
+ command: 'copy'
2123
+ });
2124
+
2125
+ /**
2126
+ * The paste action
2127
+ *
2128
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2129
+ */
2130
+ Rte.Tools.Paste = new Class(Rte.Tools.Cut, {
2131
+ command: 'paste'
2132
+ });
2133
+
2134
+ /**
2135
+ * the 'left' tool
2136
+ *
2137
+ * Copyright (C) 2010 Nikolay Nemshilov
2138
+ */
2139
+ Rte.Tools.Left = new Class(Rte.Tool.Command, {
2140
+ command: 'justifyleft'
2141
+ });
2142
+
2143
+ /**
2144
+ * the 'center' tool
2145
+ *
2146
+ * Copyright (C) 2010 Nikolay Nemshilov
2147
+ */
2148
+ Rte.Tools.Center = new Class(Rte.Tool.Command, {
2149
+ command: 'justifycenter'
2150
+ });
2151
+
2152
+ /**
2153
+ * the 'right' tool
2154
+ *
2155
+ * Copyright (C) 2010 Nikolay Nemshilov
2156
+ */
2157
+ Rte.Tools.Right = new Class(Rte.Tool.Command, {
2158
+ command: 'justifyright'
2159
+ });
2160
+
2161
+ /**
2162
+ * the 'justify' tool
2163
+ *
2164
+ * Copyright (C) 2010 Nikolay Nemshilov
2165
+ */
2166
+ Rte.Tools.Justify = new Class(Rte.Tool.Command, {
2167
+ command: 'justifyfull'
2168
+ });
2169
+
2170
+ /**
2171
+ * The undo tool
2172
+ *
2173
+ * The actual magic happens in the Rte.Undoer class
2174
+ * here we just show the status and blip it when used
2175
+ *
2176
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2177
+ */
2178
+ Rte.Tools.Undo = new Class(Rte.Tool, {
2179
+ blip: true,
2180
+
2181
+ exec: function() {
2182
+ this.rte.undoer.undo();
2183
+ },
2184
+
2185
+ enabled: function() {
2186
+ return this.rte.undoer.hasUndo();
2187
+ }
2188
+ });
2189
+
2190
+ /**
2191
+ * the 'redo' tool
2192
+ *
2193
+ * The actual magic happens in the Rte.Undoer class
2194
+ * here we just show the status and blip it when used
2195
+ *
2196
+ * Copyright (C) 2010 Nikolay Nemshilov
2197
+ */
2198
+ Rte.Tools.Redo = new Class(Rte.Tool, {
2199
+ blip: true,
2200
+
2201
+ exec: function() {
2202
+ this.rte.undoer.redo();
2203
+ },
2204
+
2205
+ enabled: function() {
2206
+ return this.rte.undoer.hasRedo();
2207
+ }
2208
+
2209
+ });
2210
+
2211
+ /**
2212
+ * The code block tool
2213
+ *
2214
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2215
+ */
2216
+ Rte.Tools.Code = new Class(Rte.Tool.Format, {
2217
+ });
2218
+
2219
+ /**
2220
+ * The block quote tool
2221
+ *
2222
+ * Copyrigth (C) 2010 Nikolay Nemshilov
2223
+ */
2224
+ Rte.Tools.Quote = new Class(Rte.Tool.Format, {
2225
+ });
2226
+
2227
+ /**
2228
+ * the 'ttext' tool
2229
+ *
2230
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2231
+ */
2232
+ Rte.Tools.Ttext = new Class(Rte.Tool.Format, {
2233
+ });
2234
+
2235
+ /**
2236
+ * The header block tool
2237
+ *
2238
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2239
+ */
2240
+ Rte.Tools.Header = new Class(Rte.Tool.Format, {
2241
+ });
2242
+
2243
+ /**
2244
+ * The the url link tool
2245
+ *
2246
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2247
+ */
2248
+ Rte.Tools.Link = new Class(Rte.Tool.Url, {
2249
+ command: 'createlink',
2250
+ attr: 'href',
2251
+
2252
+ enabled: function() {
2253
+ return !this.rte.selection.empty() || this.active();
2254
+ },
2255
+
2256
+ element: function() {
2257
+ return this.rte.status.findElement('A', {});
2258
+ }
2259
+ });
2260
+
2261
+ /**
2262
+ * The image tool
2263
+ *
2264
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2265
+ */
2266
+ Rte.Tools.Image = new Class(Rte.Tool.Url, {
2267
+ command: 'insertimage',
2268
+ attr: 'src',
2269
+
2270
+ element: function() {
2271
+ var image = this.rte.selection.element();
2272
+ return image !== null && image.tagName === "IMG" ? image : null;
2273
+ }
2274
+
2275
+ });
2276
+
2277
+ /**
2278
+ * the 'video' tool
2279
+ *
2280
+ * Copyright (C) 2010 Nikolay Nemshilov
2281
+ */
2282
+ Rte.Tools.Video = new Class(Rte.Tool.Url, {
2283
+ command: 'inserthtml',
2284
+
2285
+ enabled: function() {
2286
+ return true; // always enabled
2287
+ },
2288
+
2289
+ element: function() {
2290
+ return this.rte.status.findElement('OBJECT', {});
2291
+ },
2292
+
2293
+ // works with url address of the embedded object
2294
+ url: function(url) {
2295
+ var element = this.element() && this.element().getElementsByTagName('embed')[0];
2296
+
2297
+ if (element) {
2298
+ if (url !== undefined) {
2299
+ element.src = this.swfUrl(url);
2300
+ } else {
2301
+ return element.src;
2302
+ }
2303
+ }
2304
+ },
2305
+
2306
+ // creates the actual object block
2307
+ create: function(url) {
2308
+ var swf_url = this.swfUrl(url),
2309
+ size = 'width="'+ this.rte.options.videoSize.split('x')[0] +
2310
+ '" height="'+ this.rte.options.videoSize.split('x')[1] + '"';
2311
+
2312
+ this.$super('<object '+ size +'>'+
2313
+ '<param name="src" value="'+ swf_url +'" />'+
2314
+ '<embed src="'+ swf_url +'" type="application/x-shockwave-flash" '+ size + ' />' +
2315
+ '</object>');
2316
+ },
2317
+
2318
+ // making the actual 'swf' url
2319
+ swfUrl: function(url) {
2320
+ return R(Rte.Videos).map(function(desc) {
2321
+ return url.match(desc[0]) ?
2322
+ url.replace(desc[0], desc[1]) : null;
2323
+ }).compact()[0] || url;
2324
+ }
2325
+ });
2326
+
2327
+ /**
2328
+ * the 'dotlist' tool
2329
+ *
2330
+ * Copyright (C) 2010 Nikolay Nemshilov
2331
+ */
2332
+ Rte.Tools.Dotlist = new Class(Rte.Tool.Command, {
2333
+ command: 'insertunorderedlist'
2334
+ });
2335
+
2336
+ /**
2337
+ * the 'numlist' tool
2338
+ *
2339
+ * Copyright (C) 2010 Nikolay Nemshilov
2340
+ */
2341
+ Rte.Tools.Numlist = new Class(Rte.Tool.Command, {
2342
+ command: 'insertorderedlist'
2343
+ });
2344
+
2345
+ /**
2346
+ * the 'indent' tool
2347
+ *
2348
+ * Copyright (C) 2010 Nikolay Nemshilov
2349
+ */
2350
+ Rte.Tools.Indent = new Class(Rte.Tool.Command, {
2351
+ command: 'indent'
2352
+ });
2353
+
2354
+ /**
2355
+ * the 'Outdent' tool
2356
+ *
2357
+ * Copyright (C) 2010 Nikolay Nemshilov
2358
+ */
2359
+ Rte.Tools.Outdent = new Class(Rte.Tool.Command, {
2360
+ command: 'outdent'
2361
+ });
2362
+
2363
+ /**
2364
+ * the 'forecolor' tool
2365
+ *
2366
+ * Copyright (C) 2010 Nikolay Nemshilov
2367
+ */
2368
+ Rte.Tools.Forecolor = new Class(Rte.Tool.Color, {
2369
+ style: 'color'
2370
+ });
2371
+
2372
+ /**
2373
+ * the 'backcolor' tool
2374
+ *
2375
+ * Copyright (C) 2010 Nikolay Nemshilov
2376
+ */
2377
+ Rte.Tools.Backcolor = new Class(Rte.Tool.Color, {
2378
+ style: 'background-color'
2379
+ });
2380
+
2381
+ /**
2382
+ * The source tool
2383
+ *
2384
+ * Copyrigth (C) 2010-2011 Nikolay Nemshilov
2385
+ */
2386
+ Rte.Tools.Source = new Class(Rte.Tool, {
2387
+ source: false, // the textarea element reference
2388
+
2389
+ exec: function() {
2390
+ this[this.rte.srcMode ? 'showPreview' : 'showSource']();
2391
+ this.rte.srcMode = !this.rte.srcMode;
2392
+ },
2393
+
2394
+ active: function() {
2395
+ return this.rte.srcMode;
2396
+ },
2397
+
2398
+ // protected
2399
+
2400
+ showPreview: function() {
2401
+ this.rte.editor.setStyle('visibility:visible');
2402
+ if (this.source) {
2403
+ this.rte.value(this.source.value());
2404
+ this.source.remove();
2405
+ }
2406
+
2407
+ this.rte.editor.focus();
2408
+ },
2409
+
2410
+ showSource: function() {
2411
+ this.rte.editor.setStyle('visibility:hidden;');
2412
+
2413
+ (
2414
+ this.source = this.source ||
2415
+ $E('textarea', {'class': 'rui-rte-source'})
2416
+ )
2417
+ .insertTo(this.rte.editor, 'before')
2418
+ .resize(this.rte.editor.size())
2419
+ .setValue('' + this.rte.value())
2420
+ .focus();
2421
+
2422
+ this.rte.focused = true;
2423
+
2424
+ this.rte.status.update();
2425
+
2426
+ // locking all the tools
2427
+ for (var name in this.rte.tools) {
2428
+ if (this.rte.tools[name] !== this) {
2429
+ this.rte.tools[name].addClass('disabled');
2430
+ }
2431
+ }
2432
+ }
2433
+ });
2434
+
2435
+ /**
2436
+ * the 'clear' tool
2437
+ *
2438
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2439
+ */
2440
+ Rte.Tools.Clear = new Class(Rte.Tool, {
2441
+
2442
+ exec: function() {
2443
+ this.rte.editor.clean();
2444
+ }
2445
+
2446
+ });
2447
+
2448
+ /**
2449
+ * the 'save' tool
2450
+ *
2451
+ * Copyright (C) 2010-2011 Nikolay Nemshilov
2452
+ */
2453
+ Rte.Tools.Save = new Class(Rte.Tool, {
2454
+
2455
+ initialize: function(rte) {
2456
+ this.$super(rte);
2457
+ if (!(rte.textarea && rte.textarea.form())) {
2458
+ this.disabled = true;
2459
+ this.addClass('disabled');
2460
+ }
2461
+ },
2462
+
2463
+ exec: function() {
2464
+ if (!this.disabled) {
2465
+ this.rte.textarea.form().submit();
2466
+ }
2467
+ },
2468
+
2469
+ check: function() {} // checked in the constructor
2470
+ });
2471
+
2472
+ /**
2473
+ * Formatting style tool
2474
+ *
2475
+ * Copyright (C) 2010 Nikolay Nemshilov
2476
+ */
2477
+ Rte.Tools.Format = new Class(Rte.Tool.Format, {
2478
+ include: Rte.Tool.Options,
2479
+
2480
+ /**
2481
+ * Constructor, builds the options list and
2482
+ * defines the formats index for quick access
2483
+ *
2484
+ * @param Rte rte
2485
+ */
2486
+ initialize: function(rte) {
2487
+ var options = {}, rule, tag, match;
2488
+
2489
+ this.formats = {};
2490
+
2491
+ for (rule in Rte.Formats) {
2492
+ if ((match = rule.match(/^([a-z0-9]+)(\.([a-z0-9_\-]+))?$/))) {
2493
+ tag = match[1];
2494
+
2495
+ this.formats[rule] = { tag: tag.toUpperCase(), attrs: {}, match: {} };
2496
+
2497
+ if (match[3]) {
2498
+ this.formats[rule]['attrs']['class'] = match[3];
2499
+ this.formats[rule]['match']['class'] = new RegExp(
2500
+ '(^|\\s+)'+ RegExp.escape(match[3]) + '(\\s+|$)'
2501
+ );
2502
+ }
2503
+
2504
+ options[rule] = '<'+ tag + ' class="'+ match[3] + '">'+
2505
+ Rte.Formats[rule] + '</'+ tag + '>';
2506
+ }
2507
+ }
2508
+
2509
+ this.$super(rte).build(options);
2510
+
2511
+ return this;
2512
+ },
2513
+
2514
+ /**
2515
+ * Handling the formatting
2516
+ *
2517
+ * @return void
2518
+ */
2519
+ exec: function() {
2520
+ // removing the formatting first
2521
+ if (this.active() && this.rule) {
2522
+ this.tag = this.formats[this.rule].tag;
2523
+ this.attrs = this.formats[this.rule].attrs;
2524
+ this.unformat();
2525
+ }
2526
+
2527
+ // applying a new formatting if needed
2528
+ if (this.value && this.formats[this.value]) {
2529
+ this.tag = this.formats[this.value].tag;
2530
+ this.attrs = this.formats[this.value].attrs;
2531
+ this.format();
2532
+ }
2533
+ },
2534
+
2535
+ /**
2536
+ * Overloading the original method so that it updated the
2537
+ * currently used formatting style in the display area
2538
+ *
2539
+ * @return boolean check result
2540
+ */
2541
+ active: function() {
2542
+ var active = this.element() !== null;
2543
+ this.updateDisplay(this.rule);
2544
+ return active;
2545
+ },
2546
+
2547
+ // protected
2548
+
2549
+ // overloading the original method to handle multiple options
2550
+ element: function() {
2551
+ var rule, element, status = this.rte.status;
2552
+
2553
+ for (rule in this.formats) {
2554
+ element = status.findElement(
2555
+ this.formats[rule]['tag'],
2556
+ this.formats[rule]['match']
2557
+ );
2558
+
2559
+ if (element !== null) {
2560
+ this.rule = rule;
2561
+ return element;
2562
+ }
2563
+ }
2564
+
2565
+ return (this.rule = null);
2566
+ }
2567
+
2568
+
2569
+
2570
+ });
2571
+
2572
+ /**
2573
+ * Font-name options tool
2574
+ *
2575
+ * Copyright (C) 2010 Nikolay Nemshilov
2576
+ */
2577
+ Rte.Tools.Fontname = new Class(Rte.Tool.Style, {
2578
+ style: 'font-family',
2579
+
2580
+ /**
2581
+ * Basic constructor
2582
+ *
2583
+ * @param Rte rte
2584
+ */
2585
+ initialize: function(rte) {
2586
+ var options = {}, name, fonts = Rte.FontNames;
2587
+
2588
+ for (name in fonts) {
2589
+ options[fonts[name]] = '<div style="font-family:'+
2590
+ fonts[name]+ '">' + name + '</div>';
2591
+ }
2592
+
2593
+ return this.$super(rte, options);
2594
+ }
2595
+ });
2596
+
2597
+
2598
+ /**
2599
+ * The font-size tool
2600
+ *
2601
+ * Copyright (C) 2010 Nikolay Nemshilov
2602
+ */
2603
+ Rte.Tools.Fontsize = new Class(Rte.Tool.Style, {
2604
+ style: 'font-size',
2605
+
2606
+ /**
2607
+ * Basic constructor
2608
+ *
2609
+ * @param Rte rte
2610
+ */
2611
+ initialize: function(rte) {
2612
+ var options = {}, i=0, sizes = Rte.FontSizes.split(/\s+/);
2613
+
2614
+ for (; i < sizes.length; i++) {
2615
+ options[sizes[i]] = '<div style="font-size:'+
2616
+ sizes[i] +'">'+ sizes[i] + '</div>';
2617
+ }
2618
+
2619
+ return this.$super(rte, options);
2620
+ }
2621
+ });
2622
+
2623
+ /**
2624
+ * The subscript tool
2625
+ *
2626
+ * Copyright (C) 2010 Nikolay Nemshilov
2627
+ */
2628
+ Rte.Tools.Subscript = new Class(Rte.Tool.Command, {
2629
+ command: 'subscript'
2630
+ });
2631
+
2632
+ /**
2633
+ * The superscript tool
2634
+ *
2635
+ * Copyright (C) 2010 Nikolay Nemshilov
2636
+ */
2637
+ Rte.Tools.Superscript = new Class(Rte.Tool.Command, {
2638
+ command: 'superscript'
2639
+ });
2640
+
2641
+ /**
2642
+ * Document level hooks for the Rte widget
2643
+ *
2644
+ * Copyright (C) 2010 Nikolay Nemshilov
2645
+ */
2646
+ $(document).onReady(function() {
2647
+ $$(Rte.Options.cssRule).each('getRich');
2648
+ });
2649
+
2650
+ /**
2651
+ * Input level hooks to convert textareas into RTE
2652
+ *
2653
+ * Copyright (C) 2010 Nikolay Nemshilov
2654
+ */
2655
+ Input.include({
2656
+ /**
2657
+ * converts an input field into a RTE
2658
+ *
2659
+ * @param Object options
2660
+ * @return Rte editor
2661
+ */
2662
+ getRich: function(options) {
2663
+ if (this._.type === 'textarea' && !this.rte) {
2664
+ this.rte = new Rte(this, options);
2665
+ }
2666
+
2667
+ return this.rte;
2668
+ }
2669
+ });
2670
+
2671
+ var embed_style = document.createElement('style'),
2672
+ embed_rules = document.createTextNode("div.rui-rte,div.rui-rte-toolbar,div.rui-rte-toolbar *,div.rui-rte-editor,div.rui-rte-status,div.rui-rte-status *{margin:0;padding:0;background:none;border:none;width:auto;height:auto}textarea[data-rte]{visibility:hidden}div.rui-rte{display:inline-block; *display:inline; *zoom:1;position:relative}div.rui-rte-toolbar{padding:.15em .3em;background:#eee;border-radius:.25em .25em 0 0;-moz-border-radius:.25em .25em 0 0;-webkit-border-radius:.25em .25em 0 0;border:1px solid #ccc;border-bottom:none}div.rui-rte-toolbar div.line{display:inline-block; *display:inline; *zoom:1;margin-bottom:1px}div.rui-rte-toolbar div.bar{display:inline-block; *display:inline; *zoom:1;margin-right:2px}div.rui-rte-toolbar div.tool{display:inline-block; *display:inline; *zoom:1;margin-right:1px;vertical-align:middle;position:relative;cursor:pointer;border:1px solid #bbb;background-image:url(/images/rightjs-ui/rte.png);background-position:0px 0px;background-color:#fff;border-radius:.25em;-moz-border-radius:.25em;-webkit-border-radius:.25em}div.rui-rte-toolbar div.tool:hover{background-color:#ddd;border-color:#777}div.rui-rte-toolbar div.active{background-position:-20px 0px;background-color:#eee;border-color:#666;box-shadow:#aaa .025em .025em .5em;-moz-box-shadow:#aaa .025em .025em .5em;-webkit-box-shadow:#aaa .025em .025em .5em}div.rui-rte-toolbar div.disabled,div.rui-rte-toolbar div.disabled:hover{opacity:.4;filter:alpha(opacity=40);background-position:-40px 0px;background-color:#eee;border-color:#aaa;cursor:default}div.rui-rte-toolbar div.highlight{background-color:#BBB;border-color:#666}div.rui-rte-toolbar div.icon{height:20px;width:20px;background-image:url(/images/rightjs-ui/rte.png);background-repeat:no-repeat;background-position:20px 20px}div.rui-rte-toolbar div.save div.icon{background-position:0px -20px}div.rui-rte-toolbar div.clear div.icon{background-position:-20px -20px}div.rui-rte-toolbar div.source div.icon{background-position:-40px -20px}div.rui-rte-toolbar div.bold div.icon{background-position:0px -40px}div.rui-rte-toolbar div.italic div.icon{background-position:-20px -40px}div.rui-rte-toolbar div.underline div.icon{background-position:-40px -40px}div.rui-rte-toolbar div.strike div.icon{background-position:-60px -40px}div.rui-rte-toolbar div.cut div.icon{background-position:0px -60px}div.rui-rte-toolbar div.copy div.icon{background-position:-20px -60px}div.rui-rte-toolbar div.paste div.icon{background-position:-40px -60px}div.rui-rte-toolbar div.left div.icon{background-position:0px -80px}div.rui-rte-toolbar div.center div.icon{background-position:-20px -80px}div.rui-rte-toolbar div.right div.icon{background-position:-40px -80px}div.rui-rte-toolbar div.justify div.icon{background-position:-60px -80px}div.rui-rte-toolbar div.undo div.icon{background-position:0px -100px}div.rui-rte-toolbar div.redo div.icon{background-position:-20px -100px}div.rui-rte-toolbar div.quote div.icon{background-position:0px -120px}div.rui-rte-toolbar div.code div.icon{background-position:-20px -120px}div.rui-rte-toolbar div.ttext div.icon{background-position:-40px -120px}div.rui-rte-toolbar div.header div.icon{background-position:-60px -120px}div.rui-rte-toolbar div.image div.icon{background-position:0px -140px}div.rui-rte-toolbar div.link div.icon{background-position:-20px -140px}div.rui-rte-toolbar div.video div.icon{background-position:-40px -140px}div.rui-rte-toolbar div.dotlist div.icon{background-position:0px -160px}div.rui-rte-toolbar div.numlist div.icon{background-position:-20px -160px}div.rui-rte-toolbar div.indent div.icon{background-position:-40px -160px}div.rui-rte-toolbar div.outdent div.icon{background-position:-60px -160px}div.rui-rte-toolbar div.forecolor div.icon{background-position:0px -180px}div.rui-rte-toolbar div.backcolor div.icon{background-position:-20px -180px}div.rui-rte-toolbar div.symbol div.icon{background-position:0px -200px}div.rui-rte-toolbar div.subscript div.icon{background-position:-20px -200px}div.rui-rte-toolbar div.superscript div.icon{background-position:-40px -200px}div.rui-rte-toolbar div.with-options{padding-right:8px}div.rui-rte-toolbar div.with-options div.trigger{position:absolute;right:0;height:100%;width:7px;text-align:center;background:#ccc;border-left:1px solid #bbb}div.rui-rte-toolbar div.bar div:hover div.trigger,div.rui-rte-toolbar div.bar div.active div.trigger{background:#aaa;border-color:#888}div.rui-rte-toolbar div.with-options div.icon{display:none}div.rui-rte-toolbar div.with-options div.display{display:block;line-height:20px;padding:0 6px;margin:0;color:#222;font-size:12px;background:#f8f8f8}div.rui-rte-toolbar div.with-options ul.options,div.rui-rte-toolbar div.with-options ul.options li{list-style:none;margin:0;padding:0}div.rui-rte-toolbar div.with-options ul.options{display:none;cursor:default;position:absolute;margin-bottom:1px;margin-left:-1px;background:#fff;border:1px solid #aaa;border-radius:.25em;-moz-border-radius:.25em;-webkit-border-radius:.25em;box-shadow:#bbb .1em .1em .25em;-moz-box-shadow:#bbb .1em .1em .25em;-webkit-box-shadow:#bbb .1em .1em .25em}div.rui-rte-toolbar div.with-options ul.options li{padding:.2em .5em;white-space:nowrap;cursor:pointer}div.rui-rte-toolbar div.with-options ul.options li:hover{background-color:#eee}div.rui-rte-toolbar div.with-options ul.options li> *{margin:0;padding:0;border:none;position:static}div.rui-rte-toolbar div.color div.icon{display:block}div.rui-rte-toolbar div.color ul.options{padding-bottom:.5em}div.rui-rte-toolbar div.color ul.options li.group,div.rui-rte-toolbar div.color ul.options li.group:hover{background:none}div.rui-rte-toolbar div.color ul.options li.group ul{width:144px;clear:both;padding-top:.5em}div.rui-rte-toolbar div.color ul.options li.group ul li{float:left;width:16px;height:16px;line-height:16px;font-size:80%;text-align:center;text-indent:-9999em;padding:0;cursor:pointer;border:1px solid transparent}div.rui-rte-toolbar div.color ul.options li.group ul li:hover,div.rui-rte-toolbar div.color ul.options li.group ul li.active{background:none;border-color:#444;border-radius:.1em;-moz-border-radius:.1em;-webkit-border-radius:.1em}div.rui-rte-toolbar div.color ul.options li.group ul li.active{text-indent:0}div.rui-rte-toolbar div.color div.display{position:absolute;text-indent:-9999em;bottom:2px;left:3px;margin:0;padding:0;width:14px;height:4px;border-radius:.1em;-moz-border-radius:.1em;-webkit-border-radius:.1em}div.rui-rte-toolbar div.color ul.options li.group ul li.none{border-color:#444}div.rui-rte-toolbar div.color ul.options li.group ul li.label,div.rui-rte-toolbar div.color ul.options li.group ul li.label:hover{text-indent:0;border:none;margin-left:.5em;font-size:1em;cursor:default}div.rui-rte-editor{outline:none;outline:hidden;padding:.1em .3em;overflow:auto;background:white;border:1px solid #ccc}div.rui-rte-editor:focus{border-color:#aaa}div.rui-rte-editor> *:first-child{margin-top:0}div.rui-rte-editor> *:last-child{margin-bottom:0}div.rui-rte textarea.rui-rte-source{position:absolute;outline:none;resize:none}div.rui-rte-status{font-size:90%;height:1.4em;padding:0 .5em;color:#888;background:#eee;border:1px solid #ccc;border-top:none;border-radius:0 0 .25em .25em;-moz-border-radius:0 0 .25em .25em;-webkit-border-radius:0 0 .25em .25em}");
2673
+
2674
+ embed_style.type = 'text/css';
2675
+ document.getElementsByTagName('head')[0].appendChild(embed_style);
2676
+
2677
+ if(embed_style.styleSheet) {
2678
+ embed_style.styleSheet.cssText = embed_rules.nodeValue;
2679
+ } else {
2680
+ embed_style.appendChild(embed_rules);
2681
+ }
2682
+
2683
+
2684
+ return Rte;
2685
+ })(RightJS, document, window);