Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('content-editable', function (Y, NAME) {
2
 
3
    /*jshint maxlen: 500 */
4
    /**
5
    * Creates a component to work with an elemment.
6
    * @class ContentEditable
7
    * @for ContentEditable
8
    * @extends Y.Plugin.Base
9
    * @constructor
10
    * @module editor
11
    * @submodule content-editable
12
    */
13
 
14
    var Lang = Y.Lang,
15
        YNode = Y.Node,
16
 
17
        EVENT_CONTENT_READY = 'contentready',
18
        EVENT_READY = 'ready',
19
 
20
        TAG_PARAGRAPH = 'p',
21
 
22
        BLUR = 'blur',
23
        CONTAINER = 'container',
24
        CONTENT_EDITABLE = 'contentEditable',
25
        EMPTY = '',
26
        FOCUS = 'focus',
27
        HOST = 'host',
28
        INNER_HTML = 'innerHTML',
29
        KEY = 'key',
30
        PARENT_NODE = 'parentNode',
31
        PASTE = 'paste',
32
        TEXT = 'Text',
33
        USE = 'use',
34
 
35
    ContentEditable = function() {
36
        ContentEditable.superclass.constructor.apply(this, arguments);
37
    };
38
 
39
    Y.extend(ContentEditable, Y.Plugin.Base, {
40
 
41
        /**
42
        * Internal reference set when render is called.
43
        * @private
44
        * @property _rendered
45
        * @type Boolean
46
        */
47
        _rendered: null,
48
 
49
        /**
50
        * Internal reference to the YUI instance bound to the element
51
        * @private
52
        * @property _instance
53
        * @type YUI
54
        */
55
        _instance: null,
56
 
57
        /**
58
        * Initializes the ContentEditable instance
59
        * @protected
60
        * @method initializer
61
        */
62
        initializer: function() {
63
            var host = this.get(HOST);
64
 
65
            if (host) {
66
                host.frame = this;
67
            }
68
 
69
            this._eventHandles = [];
70
 
71
            this.publish(EVENT_READY, {
72
                emitFacade: true,
73
                defaultFn: this._defReadyFn
74
            });
75
        },
76
 
77
        /**
78
        * Destroys the instance.
79
        * @protected
80
        * @method destructor
81
        */
82
        destructor: function() {
83
            new Y.EventHandle(this._eventHandles).detach();
84
 
85
            this._container.removeAttribute(CONTENT_EDITABLE);
86
        },
87
 
88
        /**
89
        * Generic handler for all DOM events fired by the Editor container. This handler
90
        * takes the current EventFacade and augments it to fire on the ContentEditable host. It adds two new properties
91
        * to the EventFacade called frameX and frameY which adds the scroll and xy position of the ContentEditable element
92
        * to the original pageX and pageY of the event so external nodes can be positioned over the element.
93
        * In case of ContentEditable element these will be equal to pageX and pageY of the container.
94
        * @private
95
        * @method _onDomEvent
96
        * @param {EventFacade} e
97
        */
98
        _onDomEvent: function(e) {
99
            var xy;
100
 
101
            e.frameX = e.frameY = 0;
102
 
103
            if (e.pageX > 0 || e.pageY > 0) {
104
                if (e.type.substring(0, 3) !== KEY) {
105
                    xy = this._container.getXY();
106
 
107
                    e.frameX = xy[0];
108
                    e.frameY = xy[1];
109
                }
110
            }
111
 
112
            e.frameTarget = e.target;
113
            e.frameCurrentTarget = e.currentTarget;
114
            e.frameEvent = e;
115
 
116
            this.fire('dom:' + e.type, e);
117
        },
118
 
119
        /**
120
        * Simple pass thru handler for the paste event so we can do content cleanup
121
        * @private
122
        * @method _DOMPaste
123
        * @param {EventFacade} e
124
        */
125
        _DOMPaste: function(e) {
126
            var inst = this.getInstance(),
127
                data = EMPTY, win = inst.config.win;
128
 
129
            if (e._event.originalTarget) {
130
                data = e._event.originalTarget;
131
            }
132
 
133
            if (e._event.clipboardData) {
134
                data = e._event.clipboardData.getData(TEXT);
135
            }
136
 
137
            if (win.clipboardData) {
138
                data = win.clipboardData.getData(TEXT);
139
 
140
                if (data === EMPTY) { // Could be empty, or failed
141
                    // Verify failure
142
                    if (!win.clipboardData.setData(TEXT, data)) {
143
                        data = null;
144
                    }
145
                }
146
            }
147
 
148
            e.frameTarget = e.target;
149
            e.frameCurrentTarget = e.currentTarget;
150
            e.frameEvent = e;
151
 
152
            if (data) {
153
                e.clipboardData = {
154
                    data: data,
155
                    getData: function() {
156
                        return data;
157
                    }
158
                };
159
            } else {
160
                Y.log('Failed to collect clipboard data', 'warn', 'contenteditable');
161
 
162
                e.clipboardData = null;
163
            }
164
 
165
            this.fire('dom:paste', e);
166
        },
167
 
168
        /**
169
        * Binds DOM events and fires the ready event
170
        * @private
171
        * @method _defReadyFn
172
        */
173
        _defReadyFn: function() {
174
            var inst = this.getInstance(),
175
                container = this.get(CONTAINER);
176
 
177
            Y.each(
178
                ContentEditable.DOM_EVENTS,
179
                function(value, key) {
180
                    var fn = Y.bind(this._onDomEvent, this),
181
                        kfn = ((Y.UA.ie && ContentEditable.THROTTLE_TIME > 0) ? Y.throttle(fn, ContentEditable.THROTTLE_TIME) : fn);
182
 
183
                    if (!inst.Node.DOM_EVENTS[key]) {
184
                        inst.Node.DOM_EVENTS[key] = 1;
185
                    }
186
 
187
                    if (value === 1) {
188
                        if (key !== FOCUS && key !== BLUR && key !== PASTE) {
189
                            if (key.substring(0, 3) === KEY) {
190
                                //Throttle key events in IE
191
                                this._eventHandles.push(container.on(key, kfn, container));
192
                            } else {
193
                                this._eventHandles.push(container.on(key, fn, container));
194
                            }
195
                        }
196
                    }
197
                },
198
                this
199
            );
200
 
201
            inst.Node.DOM_EVENTS.paste = 1;
202
 
203
            this._eventHandles.push(
204
                container.on(PASTE, Y.bind(this._DOMPaste, this), container),
205
                container.on(FOCUS, Y.bind(this._onDomEvent, this), container),
206
                container.on(BLUR, Y.bind(this._onDomEvent, this), container)
207
            );
208
 
209
            inst.__use = inst.use;
210
 
211
            inst.use = Y.bind(this.use, this);
212
        },
213
 
214
        /**
215
        * Called once the content is available in the ContentEditable element and calls the final use call
216
        * @private
217
        * @method _onContentReady
218
        * on the internal instance so that the modules are loaded properly.
219
        */
220
        _onContentReady: function(event) {
221
            if (!this._ready) {
222
                this._ready = true;
223
 
224
                var inst = this.getInstance(),
225
                    args = Y.clone(this.get(USE));
226
 
227
                this.fire(EVENT_CONTENT_READY);
228
 
229
                Y.log('On content available', 'info', 'contenteditable');
230
 
231
                if (event) {
232
                    inst.config.doc = YNode.getDOMNode(event.target);
233
                }
234
 
235
                args.push(Y.bind(function() {
236
                    Y.log('Callback from final internal use call', 'info', 'contenteditable');
237
 
238
                    if (inst.EditorSelection) {
239
                        inst.EditorSelection.DEFAULT_BLOCK_TAG = this.get('defaultblock');
240
 
241
                        inst.EditorSelection.ROOT = this.get(CONTAINER);
242
                    }
243
 
244
                    this.fire(EVENT_READY);
245
                }, this));
246
 
247
                Y.log('Calling use on internal instance: ' + args, 'info', 'contentEditable');
248
 
249
                inst.use.apply(inst, args);
250
            }
251
        },
252
 
253
        /**
254
        * Retrieves defaultblock value from host attribute
255
        * @private
256
        * @method _getDefaultBlock
257
        * @return {String}
258
        */
259
        _getDefaultBlock: function() {
260
            return this._getHostValue('defaultblock');
261
        },
262
 
263
        /**
264
        * Retrieves dir value from host attribute
265
        * @private
266
        * @method _getDir
267
        * @return {String}
268
        */
269
        _getDir: function() {
270
            return this._getHostValue('dir');
271
        },
272
 
273
        /**
274
        * Retrieves extracss value from host attribute
275
        * @private
276
        * @method _getExtraCSS
277
        * @return {String}
278
        */
279
        _getExtraCSS: function() {
280
            return this._getHostValue('extracss');
281
        },
282
 
283
        /**
284
        * Get the content from the container
285
        * @private
286
        * @method _getHTML
287
        * @param {String} html The raw HTML from the container.
288
        * @return {String}
289
        */
290
        _getHTML: function() {
291
            var html, container;
292
 
293
            if (this._ready) {
294
                container = this.get(CONTAINER);
295
 
296
                html = container.get(INNER_HTML);
297
            }
298
 
299
            return html;
300
        },
301
 
302
        /**
303
        * Retrieves a value from host attribute
304
        * @private
305
        * @method _getHostValue
306
        * @param {attr} The attribute which value should be returned from the host
307
        * @return {String|Object}
308
        */
309
        _getHostValue: function(attr) {
310
            var host = this.get(HOST);
311
 
312
            if (host) {
313
                return host.get(attr);
314
            }
315
        },
316
 
317
        /**
318
        * Set the content of the container
319
        * @private
320
        * @method _setHTML
321
        * @param {String} html The raw HTML to set to the container.
322
        * @return {String}
323
        */
324
        _setHTML: function(html) {
325
            if (this._ready) {
326
                var container = this.get(CONTAINER);
327
 
328
                container.set(INNER_HTML, html);
329
            } else {
330
                //This needs to be wrapped in a contentready callback for the !_ready state
331
                this.once(EVENT_CONTENT_READY, Y.bind(this._setHTML, this, html));
332
            }
333
 
334
            return html;
335
        },
336
 
337
        /**
338
        * Sets the linked CSS on the instance.
339
        * @private
340
        * @method _setLinkedCSS
341
        * @param {String} css The linkedcss value
342
        * @return {String}
343
        */
344
        _setLinkedCSS: function(css) {
345
            if (this._ready) {
346
                var inst = this.getInstance();
347
                inst.Get.css(css);
348
            } else {
349
                //This needs to be wrapped in a contentready callback for the !_ready state
350
                this.once(EVENT_CONTENT_READY, Y.bind(this._setLinkedCSS, this, css));
351
            }
352
 
353
            return css;
354
        },
355
 
356
        /**
357
        * Sets the dir (language direction) attribute on the container.
358
        * @private
359
        * @method _setDir
360
        * @param {String} value The language direction
361
        * @return {String}
362
        */
363
        _setDir: function(value) {
364
            var container;
365
 
366
            if (this._ready) {
367
                container = this.get(CONTAINER);
368
 
369
                container.setAttribute('dir', value);
370
            } else {
371
                //This needs to be wrapped in a contentready callback for the !_ready state
372
                this.once(EVENT_CONTENT_READY, Y.bind(this._setDir, this, value));
373
            }
374
 
375
            return value;
376
        },
377
 
378
        /**
379
        * Set's the extra CSS on the instance.
380
        * @private
381
        * @method _setExtraCSS
382
        * @param {String} css The CSS style to be set as extra css
383
        * @return {String}
384
        */
385
        _setExtraCSS: function(css) {
386
            if (this._ready) {
387
                if (css) {
388
                    var inst = this.getInstance(),
389
                        head = inst.one('head');
390
 
391
                    if (this._extraCSSNode) {
392
                        this._extraCSSNode.remove();
393
                    }
394
 
395
                    this._extraCSSNode = YNode.create('<style>' + css + '</style>');
396
 
397
                    head.append(this._extraCSSNode);
398
                }
399
            } else {
400
                //This needs to be wrapped in a contentready callback for the !_ready state
401
                this.once(EVENT_CONTENT_READY, Y.bind(this._setExtraCSS, this, css));
402
            }
403
 
404
            return css;
405
        },
406
 
407
        /**
408
        * Sets the language value on the instance.
409
        * @private
410
        * @method _setLang
411
        * @param {String} value The language to be set
412
        * @return {String}
413
        */
414
        _setLang: function(value) {
415
            var container;
416
 
417
            if (this._ready) {
418
                container = this.get(CONTAINER);
419
 
420
                container.setAttribute('lang', value);
421
            } else {
422
                //This needs to be wrapped in a contentready callback for the !_ready state
423
                this.once(EVENT_CONTENT_READY, Y.bind(this._setLang, this, value));
424
            }
425
 
426
            return value;
427
        },
428
 
429
        /**
430
        * Called from the first YUI instance that sets up the internal instance.
431
        * This loads the content into the ContentEditable element and attaches the contentready event.
432
        * @private
433
        * @method _instanceLoaded
434
        * @param {YUI} inst The internal YUI instance bound to the ContentEditable element
435
        */
436
        _instanceLoaded: function(inst) {
437
            this._instance = inst;
438
 
439
            this._onContentReady();
440
 
441
            var doc = this._instance.config.doc;
442
 
443
            if (!Y.UA.ie) {
444
                try {
445
                    //Force other browsers into non CSS styling
446
                    doc.execCommand('styleWithCSS', false, false);
447
                    doc.execCommand('insertbronreturn', false, false);
448
                } catch (err) {}
449
            }
450
        },
451
 
452
 
453
        /**
454
        * Validates linkedcss property
455
        *
456
        * @method _validateLinkedCSS
457
        * @private
458
        */
459
        _validateLinkedCSS: function(value) {
460
            return Lang.isString(value) || Lang.isArray(value);
461
        },
462
 
463
        //BEGIN PUBLIC METHODS
464
        /**
465
        * This is a scoped version of the normal YUI.use method & is bound to the ContentEditable element
466
        * At setup, the inst.use method is mapped to this method.
467
        * @method use
468
        */
469
        use: function() {
470
            Y.log('Calling augmented use after ready', 'info', 'contenteditable');
471
 
472
            var inst = this.getInstance(),
473
                args = Y.Array(arguments),
474
                callback = false;
475
 
476
            if (Lang.isFunction(args[args.length - 1])) {
477
                callback = args.pop();
478
            }
479
 
480
            if (callback) {
481
                args.push(function() {
482
                    Y.log('Internal callback from augmented use', 'info', 'contenteditable');
483
 
484
                    callback.apply(inst, arguments);
485
                });
486
            }
487
 
488
            return inst.__use.apply(inst, args);
489
        },
490
 
491
        /**
492
        * A delegate method passed to the instance's delegate method
493
        * @method delegate
494
        * @param {String} type The type of event to listen for
495
        * @param {Function} fn The method to attach
496
        * @param {String, Node} cont The container to act as a delegate, if no "sel" passed, the container is assumed.
497
        * @param {String} sel The selector to match in the event (optional)
498
        * @return {EventHandle} The Event handle returned from Y.delegate
499
        */
500
        delegate: function(type, fn, cont, sel) {
501
            var inst = this.getInstance();
502
 
503
            if (!inst) {
504
                Y.log('Delegate events can not be attached until after the ready event has fired.', 'error', 'contenteditable');
505
 
506
                return false;
507
            }
508
 
509
            if (!sel) {
510
                sel = cont;
511
 
512
                cont = this.get(CONTAINER);
513
            }
514
 
515
            return inst.delegate(type, fn, cont, sel);
516
        },
517
 
518
        /**
519
        * Get a reference to the internal YUI instance.
520
        * @method getInstance
521
        * @return {YUI} The internal YUI instance
522
        */
523
        getInstance: function() {
524
            return this._instance;
525
        },
526
 
527
        /**
528
        * @method render
529
        * @param {String/HTMLElement/Node} node The node to render to
530
        * @return {ContentEditable}
531
        * @chainable
532
        */
533
        render: function(node) {
534
            var args, inst, fn;
535
 
536
            if (this._rendered) {
537
                Y.log('Container already rendered.', 'warn', 'contentEditable');
538
 
539
                return this;
540
            }
541
 
542
            if (node) {
543
                this.set(CONTAINER, node);
544
            }
545
 
546
            container = this.get(CONTAINER);
547
 
548
            if (!container) {
549
                container = YNode.create(ContentEditable.HTML);
550
 
551
                Y.one('body').prepend(container);
552
 
553
                this.set(CONTAINER, container);
554
            }
555
 
556
            this._rendered = true;
557
 
558
            this._container.setAttribute(CONTENT_EDITABLE, true);
559
 
560
            args = Y.clone(this.get(USE));
561
 
562
            fn = Y.bind(function() {
563
                inst = YUI();
564
 
565
                inst.host = this.get(HOST); //Cross reference to Editor
566
 
567
                inst.log = Y.log; //Dump the instance logs to the parent instance.
568
 
569
                Y.log('Creating new internal instance with node-base only', 'info', 'contenteditable');
570
                inst.use('node-base', Y.bind(this._instanceLoaded, this));
571
            }, this);
572
 
573
            args.push(fn);
574
 
575
            Y.log('Adding new modules to main instance: ' + args, 'info', 'contenteditable');
576
            Y.use.apply(Y, args);
577
 
578
            return this;
579
        },
580
 
581
        /**
582
        * Set the focus to the container
583
        * @method focus
584
        * @param {Function} fn Callback function to execute after focus happens
585
        * @return {ContentEditable}
586
        * @chainable
587
        */
588
        focus: function() {
589
            this._container.focus();
590
 
591
            return this;
592
        },
593
        /**
594
        * Show the iframe instance
595
        * @method show
596
        * @return {ContentEditable}
597
        * @chainable
598
        */
599
        show: function() {
600
            this._container.show();
601
 
602
            this.focus();
603
 
604
            return this;
605
        },
606
 
607
        /**
608
        * Hide the iframe instance
609
        * @method hide
610
        * @return {ContentEditable}
611
        * @chainable
612
        */
613
        hide: function() {
614
            this._container.hide();
615
 
616
            return this;
617
        }
618
    },
619
    {
620
        /**
621
        * The throttle time for key events in IE
622
        * @static
623
        * @property THROTTLE_TIME
624
        * @type Number
625
        * @default 100
626
        */
627
        THROTTLE_TIME: 100,
628
 
629
        /**
630
        * The DomEvents that the frame automatically attaches and bubbles
631
        * @static
632
        * @property DOM_EVENTS
633
        * @type Object
634
        */
635
        DOM_EVENTS: {
636
            click: 1,
637
            dblclick: 1,
638
            focusin: 1,
639
            focusout: 1,
640
            keydown: 1,
641
            keypress: 1,
642
            keyup: 1,
643
            mousedown: 1,
644
            mouseup: 1,
645
            paste: 1
646
        },
647
 
648
        /**
649
        * The template string used to create the ContentEditable element
650
        * @static
651
        * @property HTML
652
        * @type String
653
        */
654
        HTML: '<div></div>',
655
 
656
        /**
657
        * The name of the class (contentEditable)
658
        * @static
659
        * @property NAME
660
        * @type String
661
        */
662
        NAME: 'contentEditable',
663
 
664
        /**
665
        * The namespace on which ContentEditable plugin will reside.
666
        *
667
        * @property NS
668
        * @type String
669
        * @default 'contentEditable'
670
        * @static
671
        */
672
        NS: CONTENT_EDITABLE,
673
 
674
        ATTRS: {
675
            /**
676
            * The default text direction for this ContentEditable element. Default: ltr
677
            * @attribute dir
678
            * @type String
679
            */
680
            dir: {
681
                lazyAdd: false,
682
                validator: Lang.isString,
683
                setter: '_setDir',
684
                valueFn: '_getDir'
685
            },
686
 
687
            /**
688
            * The container to set contentEditable=true or to create on render.
689
            * @attribute container
690
            * @type String/HTMLElement/Node
691
            */
692
            container: {
693
                setter: function(n) {
694
                    this._container = Y.one(n);
695
 
696
                    return this._container;
697
                }
698
            },
699
 
700
            /**
701
            * The string to inject as Editor content. Default '<br>'
702
            * @attribute content
703
            * @type String
704
            */
705
            content: {
706
                getter: '_getHTML',
707
                lazyAdd: false,
708
                setter: '_setHTML',
709
                validator: Lang.isString,
710
                value: '<br>'
711
            },
712
 
713
            /**
714
            * The default tag to use for block level items, defaults to: p
715
            * @attribute defaultblock
716
            * @type String
717
            */
718
            defaultblock: {
719
                validator: Lang.isString,
720
                value: TAG_PARAGRAPH,
721
                valueFn: '_getDefaultBlock'
722
            },
723
 
724
            /**
725
            * A string of CSS to add to the Head of the Editor
726
            * @attribute extracss
727
            * @type String
728
            */
729
            extracss: {
730
                lazyAdd: false,
731
                setter: '_setExtraCSS',
732
                validator: Lang.isString,
733
                valueFn: '_getExtraCSS'
734
            },
735
 
736
            /**
737
            * Set the id of the new Node. (optional)
738
            * @attribute id
739
            * @type String
740
            * @writeonce
741
            */
742
            id: {
743
                writeOnce: true,
744
                getter: function(id) {
745
                    if (!id) {
746
                        id = 'inlineedit-' + Y.guid();
747
                    }
748
 
749
                    return id;
750
                }
751
            },
752
 
753
            /**
754
            * The default language. Default: en-US
755
            * @attribute lang
756
            * @type String
757
            */
758
            lang: {
759
                validator: Lang.isString,
760
                setter: '_setLang',
761
                lazyAdd: false,
762
                value: 'en-US'
763
            },
764
 
765
            /**
766
            * An array of url's to external linked style sheets
767
            * @attribute linkedcss
768
            * @type String|Array
769
            */
770
            linkedcss: {
771
                setter: '_setLinkedCSS',
772
                validator: '_validateLinkedCSS'
773
                //value: ''
774
            },
775
 
776
            /**
777
            * The Node instance of the container.
778
            * @attribute node
779
            * @type Node
780
            */
781
            node: {
782
                readOnly: true,
783
                value: null,
784
                getter: function() {
785
                    return this._container;
786
                }
787
            },
788
 
789
            /**
790
            * Array of modules to include in the scoped YUI instance at render time. Default: ['node-base', 'editor-selection', 'stylesheet']
791
            * @attribute use
792
            * @writeonce
793
            * @type Array
794
            */
795
            use: {
796
                validator: Lang.isArray,
797
                writeOnce: true,
798
                value: ['node-base', 'editor-selection', 'stylesheet']
799
            }
800
        }
801
    });
802
 
803
    Y.namespace('Plugin');
804
 
805
    Y.Plugin.ContentEditable = ContentEditable;
806
 
807
 
808
}, '3.18.1', {"requires": ["node-base", "editor-selection", "stylesheet", "plugin"]});