Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
/**
16
 * @module moodle-editor_atto-editor
17
 * @submodule selection
18
 */
19
 
20
/**
21
 * Selection functions for the Atto editor.
22
 *
23
 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
24
 *
25
 * @namespace M.editor_atto
26
 * @class EditorSelection
27
 */
28
 
29
function EditorSelection() {}
30
 
31
EditorSelection.ATTRS = {
32
};
33
 
34
EditorSelection.prototype = {
35
 
36
    /**
37
     * List of saved selections per editor instance.
38
     *
39
     * @property _selections
40
     * @private
41
     */
42
    _selections: null,
43
 
44
    /**
45
     * A unique identifier for the last selection recorded.
46
     *
47
     * @property _lastSelection
48
     * @param lastselection
49
     * @type string
50
     * @private
51
     */
52
    _lastSelection: null,
53
 
54
    /**
55
     * Whether focus came from a click event.
56
     *
57
     * This is used to determine whether to restore the selection or not.
58
     *
59
     * @property _focusFromClick
60
     * @type Boolean
61
     * @default false
62
     * @private
63
     */
64
    _focusFromClick: false,
65
 
66
    /**
67
     * Whether if the last gesturemovestart event target was contained in this editor or not.
68
     *
69
     * @property _gesturestartededitor
70
     * @type Boolean
71
     * @default false
72
     * @private
73
     */
74
    _gesturestartededitor: false,
75
 
76
    /**
77
     * Set up the watchers for selection save and restoration.
78
     *
79
     * @method setupSelectionWatchers
80
     * @chainable
81
     */
82
    setupSelectionWatchers: function() {
83
        // Save the selection when a change was made.
84
        this.on('atto:selectionchanged', this.saveSelection, this);
85
 
86
        this.editor.on('focus', this.restoreSelection, this);
87
 
88
        // Do not restore selection when focus is from a click event.
89
        this.editor.on('mousedown', function() {
90
            this._focusFromClick = true;
91
        }, this);
92
 
93
        // Copy the current value back to the textarea when focus leaves us and save the current selection.
94
        this.editor.on('blur', function() {
95
            // Clear the _focusFromClick value.
96
            this._focusFromClick = false;
97
 
98
            // Update the original text area.
99
            this.updateOriginal();
100
        }, this);
101
 
102
        this.editor.on(['keyup', 'focus'], function(e) {
103
                Y.soon(Y.bind(this._hasSelectionChanged, this, e));
104
            }, this);
105
 
106
        Y.one(document.body).on('gesturemovestart', function(e) {
107
            if (this._wrapper.contains(e.target._node)) {
108
                this._gesturestartededitor = true;
109
            } else {
110
                this._gesturestartededitor = false;
111
            }
112
        }, null, this);
113
 
114
        Y.one(document.body).on('gesturemoveend', function(e) {
115
            if (!this._gesturestartededitor) {
116
                // Ignore the event if movestart target was not contained in the editor.
117
                return;
118
            }
119
            Y.soon(Y.bind(this._hasSelectionChanged, this, e));
120
        }, {
121
            // Standalone will make sure all editors receive the end event.
122
            standAlone: true
123
        }, this);
124
 
125
        return this;
126
    },
127
 
128
    /**
129
     * Work out if the cursor is in the editable area for this editor instance.
130
     *
131
     * @method isActive
132
     * @return {boolean}
133
     */
134
    isActive: function() {
135
        var range = rangy.createRange(),
136
            selection = rangy.getSelection();
137
 
138
        if (!selection.rangeCount) {
139
            // If there was no range count, then there is no selection.
140
            return false;
141
        }
142
 
143
        // We can't be active if the editor doesn't have focus at the moment.
144
        if (!document.activeElement ||
145
                !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {
146
            return false;
147
        }
148
 
149
        // Check whether the range intersects the editor selection.
150
        range.selectNode(this.editor.getDOMNode());
151
        return range.intersectsRange(selection.getRangeAt(0));
152
    },
153
 
154
    /**
155
     * Create a cross browser selection object that represents a YUI node.
156
     *
157
     * @method getSelectionFromNode
158
     * @param {Node} YUI Node to base the selection upon.
159
     * @return {[rangy.Range]}
160
     */
161
    getSelectionFromNode: function(node) {
162
        var range = rangy.createRange();
163
        range.selectNode(node.getDOMNode());
164
        return [range];
165
    },
166
 
167
    /**
168
     * Save the current selection to an internal property.
169
     *
170
     * This allows more reliable return focus, helping improve keyboard navigation.
171
     *
172
     * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.
173
     *
174
     * @method saveSelection
175
     */
176
    saveSelection: function() {
177
        if (this.isActive()) {
178
            this._selections = this.getSelection();
179
        }
180
    },
181
 
182
    /**
183
     * Restore any stored selection when the editor gets focus again.
184
     *
185
     * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.
186
     *
187
     * @method restoreSelection
188
     */
189
    restoreSelection: function() {
190
        if (!this._focusFromClick) {
191
            if (this._selections) {
192
                this.setSelection(this._selections);
193
            }
194
        }
195
        this._focusFromClick = false;
196
    },
197
 
198
    /**
199
     * Get the selection object that can be passed back to setSelection.
200
     *
201
     * @method getSelection
202
     * @return {array} An array of rangy ranges.
203
     */
204
    getSelection: function() {
205
        return rangy.getSelection().getAllRanges();
206
    },
207
 
208
    /**
209
     * Check that a YUI node it at least partly contained by the current selection.
210
     *
211
     * @method selectionContainsNode
212
     * @param {Node} The node to check.
213
     * @return {boolean}
214
     */
215
    selectionContainsNode: function(node) {
216
        return rangy.getSelection().containsNode(node.getDOMNode(), true);
217
    },
218
 
219
    /**
220
     * Runs a filter on each node in the selection, and report whether the
221
     * supplied selector(s) were found in the supplied Nodes.
222
     *
223
     * By default, all specified nodes must match the selection, but this
224
     * can be controlled with the requireall property.
225
     *
226
     * @method selectionFilterMatches
227
     * @param {String} selector
228
     * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.
229
     * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.
230
     * @return {Boolean}
231
     */
232
    selectionFilterMatches: function(selector, selectednodes, requireall) {
233
        if (typeof requireall === 'undefined') {
234
            requireall = true;
235
        }
236
        if (!selectednodes) {
237
            // Find this because it was not passed as a param.
238
            selectednodes = this.getSelectedNodes();
239
        }
240
        var allmatch = selectednodes.size() > 0,
241
            anymatch = false;
242
 
243
        var editor = this.editor,
244
            stopFn = function(node) {
245
                // The function getSelectedNodes only returns nodes within the editor, so this test is safe.
246
                return node === editor;
247
            };
248
 
249
        // If we do not find at least one match in the editor, no point trying to find them in the selection.
250
        if (!editor.one(selector)) {
251
            return false;
252
        }
253
 
254
        selectednodes.each(function(node) {
255
            // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
256
            if (requireall) {
257
                // Check for at least one failure.
258
                if (!allmatch || !node.ancestor(selector, true, stopFn)) {
259
                    allmatch = false;
260
                }
261
            } else {
262
                // Check for at least one match.
263
                if (!anymatch && node.ancestor(selector, true, stopFn)) {
264
                    anymatch = true;
265
                }
266
            }
267
        }, this);
268
        if (requireall) {
269
            return allmatch;
270
        } else {
271
            return anymatch;
272
        }
273
    },
274
 
275
    /**
276
     * Get the deepest possible list of nodes in the current selection.
277
     *
278
     * @method getSelectedNodes
279
     * @return {NodeList}
280
     */
281
    getSelectedNodes: function() {
282
        var results = new Y.NodeList(),
283
            nodes,
284
            selection,
285
            range,
286
            node,
287
            i;
288
 
289
        selection = rangy.getSelection();
290
 
291
        if (selection.rangeCount) {
292
            range = selection.getRangeAt(0);
293
        } else {
294
            // Empty range.
295
            range = rangy.createRange();
296
        }
297
 
298
        if (range.collapsed) {
299
            // We do not want to select all the nodes in the editor if we managed to
300
            // have a collapsed selection directly in the editor.
301
            // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle
302
            // so we must filter that out here too.
303
            if (range.commonAncestorContainer !== this.editor.getDOMNode()
304
                    && range.commonAncestorContainer !== Y.config.doc) {
305
                range = range.cloneRange();
306
                range.selectNode(range.commonAncestorContainer);
307
            }
308
        }
309
 
310
        nodes = range.getNodes();
311
 
312
        for (i = 0; i < nodes.length; i++) {
313
            node = Y.one(nodes[i]);
314
            if (this.editor.contains(node)) {
315
                results.push(node);
316
            }
317
        }
318
        return results;
319
    },
320
 
321
    /**
322
     * Check whether the current selection has changed since this method was last called.
323
     *
324
     * If the selection has changed, the atto:selectionchanged event is also fired.
325
     *
326
     * @method _hasSelectionChanged
327
     * @private
328
     * @param {EventFacade} e
329
     * @return {Boolean}
330
     */
331
    _hasSelectionChanged: function(e) {
332
        var selection = rangy.getSelection(),
333
            range,
334
            changed = false;
335
 
336
        if (selection.rangeCount) {
337
            range = selection.getRangeAt(0);
338
        } else {
339
            // Empty range.
340
            range = rangy.createRange();
341
        }
342
 
343
        if (this._lastSelection) {
344
            if (!this._lastSelection.equals(range)) {
345
                changed = true;
346
                return this._fireSelectionChanged(e);
347
            }
348
        }
349
        this._lastSelection = range;
350
        return changed;
351
    },
352
 
353
    /**
354
     * Fires the atto:selectionchanged event.
355
     *
356
     * When the selectionchanged event is fired, the following arguments are provided:
357
     *   - event : the original event that lead to this event being fired.
358
     *   - selectednodes :  an array containing nodes that are entirely selected of contain partially selected content.
359
     *
360
     * @method _fireSelectionChanged
361
     * @private
362
     * @param {EventFacade} e
363
     */
364
    _fireSelectionChanged: function(e) {
365
        this.fire('atto:selectionchanged', {
366
            event: e,
367
            selectedNodes: this.getSelectedNodes()
368
        });
369
    },
370
 
371
    /**
372
     * Get the DOM node representing the common anscestor of the selection nodes.
373
     *
374
     * @method getSelectionParentNode
375
     * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.
376
     */
377
    getSelectionParentNode: function() {
378
        var selection = rangy.getSelection();
379
        if (selection.rangeCount) {
380
            return selection.getRangeAt(0).commonAncestorContainer;
381
        }
382
        return false;
383
    },
384
 
385
    /**
386
     * Set the current selection. Used to restore a selection.
387
     *
388
     * @method selection
389
     * @param {array} ranges A list of rangy.range objects in the selection.
390
     */
391
    setSelection: function(ranges) {
392
        var selection = rangy.getSelection();
393
        selection.setRanges(ranges);
394
    },
395
 
396
    /**
397
     * Inserts the given HTML into the editable content at the currently focused point.
398
     *
399
     * @method insertContentAtFocusPoint
400
     * @param {String} html
401
     * @return {Node} The YUI Node object added to the DOM.
402
     */
403
    insertContentAtFocusPoint: function(html) {
404
        var selection = rangy.getSelection(),
405
            range,
406
            node = Y.Node.create(html);
407
        if (selection.rangeCount) {
408
            range = selection.getRangeAt(0);
409
        }
410
        if (range) {
411
            range.deleteContents();
412
            range.collapse(false);
413
            var currentnode = node.getDOMNode(),
414
                last = currentnode.lastChild || currentnode;
415
            range.insertNode(currentnode);
416
            range.collapseAfter(last);
417
            selection.setSingleRange(range);
418
        }
419
        return node;
420
    }
421
 
422
};
423
 
424
Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);