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