Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('moodle-atto_link-button', function (Y, NAME) {
2
 
3
// This file is part of Moodle - http://moodle.org/
4
//
5
// Moodle is free software: you can redistribute it and/or modify
6
// it under the terms of the GNU General Public License as published by
7
// the Free Software Foundation, either version 3 of the License, or
8
// (at your option) any later version.
9
//
10
// Moodle is distributed in the hope that it will be useful,
11
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
// GNU General Public License for more details.
14
//
15
// You should have received a copy of the GNU General Public License
16
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17
 
18
/*
19
 * @package    atto_link
20
 * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
/**
25
 * @module moodle-atto_link-button
26
 */
27
 
28
/**
29
 * Atto text editor link plugin.
30
 *
31
 * @namespace M.atto_link
32
 * @class button
33
 * @extends M.editor_atto.EditorPlugin
34
 */
35
 
36
var COMPONENTNAME = 'atto_link',
37
    CSS = {
38
        NEWWINDOW: 'atto_link_openinnewwindow',
39
        URLINPUT: 'atto_link_urlentry',
40
        URLTEXT: 'atto_link_urltext'
41
    },
42
    SELECTORS = {
43
        NEWWINDOW: '.atto_link_openinnewwindow',
44
        URLINPUT: '.atto_link_urlentry',
45
        URLTEXT: '.atto_link_urltext',
46
        SUBMIT: '.submit',
47
        LINKBROWSER: '.openlinkbrowser'
48
    },
49
    TEMPLATE = '' +
50
            '<form class="atto_form">' +
51
                '<div class="mb-1">' +
52
                '<label for="{{elementid}}_atto_link_urltext">{{get_string "texttodisplay" component}}</label>' +
53
                '<input class="form-control fullwidth {{CSS.URLTEXT}}" type="text" ' +
54
                'id="{{elementid}}_atto_link_urltext" size="32"/>' +
55
                '</div>' +
56
                '{{#if showFilepicker}}' +
57
                    '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
58
                    '<div class="input-group input-append w-100 mb-1">' +
59
                        '<input class="form-control url {{CSS.URLINPUT}}" type="url" ' +
60
                        'id="{{elementid}}_atto_link_urlentry"/>' +
61
                        '<span class="input-group-append">' +
62
                            '<button class="btn btn-secondary openlinkbrowser" type="button">' +
63
                            '{{get_string "browserepositories" component}}</button>' +
64
                        '</span>' +
65
                    '</div>' +
66
                '{{else}}' +
67
                    '<div class="mb-1">' +
68
                        '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
69
                        '<input class="form-control fullwidth url {{CSS.URLINPUT}}" type="url" ' +
70
                        'id="{{elementid}}_atto_link_urlentry" size="32"/>' +
71
                    '</div>' +
72
                '{{/if}}' +
73
                '<div class="form-check">' +
74
                    '<input type="checkbox" class="form-check-input newwindow {{CSS.NEWWINDOW}}" ' +
75
                    'id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
76
                    '<label class="form-check-label" for="{{elementid}}_{{CSS.NEWWINDOW}}">' +
77
                    '{{get_string "openinnewwindow" component}}' +
78
                    '</label>' +
79
                '</div>' +
80
                '<div class="mdl-align">' +
81
                    '<br/>' +
82
                    '<button type="submit" class="btn btn-secondary submit">{{get_string "createlink" component}}</button>' +
83
                '</div>' +
84
            '</form>';
85
Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
86
 
87
    /**
88
     * A reference to the current selection at the time that the dialogue
89
     * was opened.
90
     *
91
     * @property _currentSelection
92
     * @type Range
93
     * @private
94
     */
95
    _currentSelection: null,
96
 
97
    /**
98
     * A reference to the dialogue content.
99
     *
100
     * @property _content
101
     * @type Node
102
     * @private
103
     */
104
    _content: null,
105
 
106
    /**
107
     * Text to display has value or not.
108
     * @property _hasTextToDisplay
109
     * @type Boolean
110
     * @private
111
     */
112
    _hasTextToDisplay: false,
113
 
114
    /**
115
     * User has selected plain text or not.
116
     * @property _hasPlainTextSelected
117
     * @type Boolean
118
     * @private
119
     */
120
    _hasPlainTextSelected: false,
121
 
122
    initializer: function() {
123
        // Add the link button first.
124
        this.addButton({
125
            icon: 'e/insert_edit_link',
126
            keys: '75',
127
            callback: this._displayDialogue,
128
            tags: 'a',
129
            tagMatchRequiresAll: false
130
        });
131
 
132
        // And then the unlink button.
133
        this.addButton({
134
            buttonName: 'unlink',
135
            callback: this._unlink,
136
            icon: 'e/remove_link',
137
            title: 'unlink',
138
 
139
            // Watch the following tags and add/remove highlighting as appropriate:
140
            tags: 'a',
141
            tagMatchRequiresAll: false
142
        });
143
    },
144
 
145
    /**
146
     * Display the link editor.
147
     *
148
     * @method _displayDialogue
149
     * @private
150
     */
151
    _displayDialogue: function() {
152
        // Store the current selection.
153
        this._currentSelection = this.get('host').getSelection();
154
        if (this._currentSelection === false) {
155
            return;
156
        }
157
 
158
        var dialogue = this.getDialogue({
159
            headerContent: M.util.get_string('createlink', COMPONENTNAME),
160
            width: 'auto',
161
            focusAfterHide: true,
162
            focusOnShowSelector: SELECTORS.URLINPUT
163
        });
164
 
165
        // Set the dialogue content, and then show the dialogue.
166
        dialogue.set('bodyContent', this._getDialogueContent());
167
 
168
        // Resolve anchors in the selected text.
169
        this._resolveAnchors();
170
        dialogue.show();
171
    },
172
 
173
    /**
174
     * If there is selected text and it is part of an anchor link,
175
     * extract the url (and target) from the link (and set them in the form).
176
     *
177
     * @method _resolveAnchors
178
     * @private
179
     */
180
    _resolveAnchors: function() {
181
        // Find the first anchor tag in the selection.
182
        var selectednode = this.get('host').getSelectionParentNode(),
183
            anchornodes,
184
            anchornode,
185
            url,
186
            target,
187
            textToDisplay,
188
            title;
189
 
190
        // Note this is a document fragment and YUI doesn't like them.
191
        if (!selectednode) {
192
            return;
193
        }
194
 
195
        anchornodes = this._findSelectedAnchors(Y.one(selectednode));
196
        if (anchornodes.length > 0) {
197
            anchornode = anchornodes[0];
198
            this._currentSelection = this.get('host').getSelectionFromNode(anchornode);
199
            url = anchornode.getAttribute('href');
200
            target = anchornode.getAttribute('target');
201
            textToDisplay = anchornode.get('innerText');
202
            title = anchornode.getAttribute('title');
203
            if (url !== '') {
204
                this._content.one(SELECTORS.URLINPUT).setAttribute('value', url);
205
            }
206
            if (textToDisplay !== '') {
207
                this._content.one(SELECTORS.URLTEXT).set('value', textToDisplay);
208
            } else if (title !== '') {
209
                this._content.one(SELECTORS.URLTEXT).set('value', title);
210
            }
211
            if (target === '_blank') {
212
                this._content.one(SELECTORS.NEWWINDOW).setAttribute('checked', 'checked');
213
            } else {
214
                this._content.one(SELECTORS.NEWWINDOW).removeAttribute('checked');
215
            }
216
        } else {
217
            // User is selecting some text before clicking on the Link button.
218
            textToDisplay = this._getTextSelection();
219
            if (textToDisplay !== '') {
220
                this._hasTextToDisplay = true;
221
                this._hasPlainTextSelected = true;
222
                this._content.one(SELECTORS.URLTEXT).set('value', textToDisplay);
223
            }
224
        }
225
    },
226
 
227
    /**
228
     * Update the dialogue after a link was selected in the File Picker.
229
     *
230
     * @method _filepickerCallback
231
     * @param {object} params The parameters provided by the filepicker
232
     * containing information about the link.
233
     * @private
234
     */
235
    _filepickerCallback: function(params) {
236
        this.getDialogue()
237
                .set('focusAfterHide', null)
238
                .hide();
239
 
240
        if (params.url !== '') {
241
            // Add the link.
242
            this._setLinkOnSelection(params.url);
243
 
244
            // And mark the text area as updated.
245
            this.markUpdated();
246
        }
247
    },
248
 
249
    /**
250
     * The link was inserted, so make changes to the editor source.
251
     *
252
     * @method _setLink
253
     * @param {EventFacade} e
254
     * @private
255
     */
256
    _setLink: function(e) {
257
        var input,
258
            value;
259
 
260
        e.preventDefault();
261
        this.getDialogue({
262
            focusAfterHide: null
263
        }).hide();
264
 
265
        input = this._content.one(SELECTORS.URLINPUT);
266
 
267
        value = input.get('value');
268
        if (value !== '') {
269
 
270
            // We add a prefix if it is not already prefixed.
271
            value = value.trim();
272
            var expr = new RegExp(/^[a-zA-Z]*\.*\/|^#|^[a-zA-Z]*:/);
273
            if (!expr.test(value)) {
274
                value = 'http://' + value;
275
            }
276
 
277
            // Add the link.
278
            this._setLinkOnSelection(value);
279
 
280
            this.markUpdated();
281
        }
282
    },
283
 
284
    /**
285
     * Final step setting the anchor on the selection.
286
     *
287
     * @private
288
     * @method _setLinkOnSelection
289
     * @param  {String} url URL the link will point to.
290
     * @return {Node} The added Node.
291
     */
292
    _setLinkOnSelection: function(url) {
293
        var host = this.get('host'),
294
            link,
295
            selectednode,
296
            target,
297
            anchornodes,
298
            isUpdating,
299
            urlText,
300
            textToDisplay;
301
 
302
        this.editor.focus();
303
        host.setSelection(this._currentSelection);
304
        isUpdating = !this._currentSelection[0].collapsed;
305
        urlText = this._content.one(SELECTORS.URLTEXT);
306
        textToDisplay = urlText.get('value').replace(/(<([^>]+)>)/gi, "").trim();
307
        if (textToDisplay === '') {
308
            textToDisplay = url;
309
        }
310
 
311
        if (!isUpdating) {
312
            // Firefox cannot add links when the selection is empty so we will add it manually.
313
            link = Y.Node.create('<a>' + textToDisplay + '</a>');
314
            link.setAttribute('href', url);
315
 
316
            // Add the node and select it to replicate the behaviour of execCommand.
317
            selectednode = host.insertContentAtFocusPoint(link.get('outerHTML'));
318
            host.setSelection(host.getSelectionFromNode(selectednode));
319
        } else {
11 efrain 320
            if (Y.UA.gecko > 0) {
321
                // For Firefox / Gecko we need to wrap the selection in a span so we can surround it with an anchor.
322
                // This relates to https://bugzilla.mozilla.org/show_bug.cgi?id=1906559.
323
                var originalSelection = document.getSelection();
324
                var wrapper = document.createElement('span');
325
                wrapper.setAttribute('data-wrapper', '');
326
                wrapper.style.display = 'inline';
1 efrain 327
 
11 efrain 328
                var i;
329
                for (i = 0; i < originalSelection.rangeCount; i++) {
330
                    originalSelection.getRangeAt(i).surroundContents(wrapper);
331
                }
332
                host.setSelection(host.getSelectionFromNode(Y.one(wrapper)));
333
 
334
                document.execCommand('unlink', false, null);
335
                document.execCommand('createLink', false, url);
336
 
337
                var anchorNode = wrapper.parentNode;
338
                wrapper.children.forEach(function(child) {
339
                    anchorNode.appendChild(child);
340
                });
341
                wrapper.remove();
342
 
343
            } else {
344
                document.execCommand('unlink', false, null);
345
                document.execCommand('createLink', false, url);
346
            }
347
 
1 efrain 348
            // Now set the target.
349
            selectednode = host.getSelectionParentNode();
350
        }
351
 
352
        // Note this is a document fragment and YUI doesn't like them.
353
        if (!selectednode) {
354
            return;
355
        }
356
 
357
        anchornodes = this._findSelectedAnchors(Y.one(selectednode));
358
        // Add new window attributes if requested.
359
        Y.Array.each(anchornodes, function(anchornode) {
360
            target = this._content.one(SELECTORS.NEWWINDOW);
361
            if (target.get('checked')) {
362
                anchornode.setAttribute('target', '_blank');
363
            } else {
364
                anchornode.removeAttribute('target');
365
            }
366
            if (isUpdating && textToDisplay) {
367
                // The 'createLink' command do not allow to set the custom text to display. So we need to do it here.
368
                if (this._hasPlainTextSelected) {
369
                    // Only replace the innerText if the user has not selected any element or just the plain text.
370
                    anchornode.set('innerText', textToDisplay);
371
                } else {
372
                    // The user has selected another element to add the hyperlink, like an image.
373
                    // We should add the title attribute instead of replacing the innerText of the hyperlink.
374
                    anchornode.setAttribute('title', textToDisplay);
375
                }
376
            }
377
        }, this);
378
 
379
        return selectednode;
380
    },
381
 
382
    /**
383
     * Look up and down for the nearest anchor tags that are least partly contained in the selection.
384
     *
385
     * @method _findSelectedAnchors
386
     * @param {Node} node The node to search under for the selected anchor.
387
     * @return {Node|Boolean} The Node, or false if not found.
388
     * @private
389
     */
390
    _findSelectedAnchors: function(node) {
391
        var tagname = node.get('tagName'),
392
            hit, hits;
393
 
394
        // Direct hit.
395
        if (tagname && tagname.toLowerCase() === 'a') {
396
            return [node];
397
        }
398
 
399
        // Search down but check that each node is part of the selection.
400
        hits = [];
401
        node.all('a').each(function(n) {
402
            if (!hit && this.get('host').selectionContainsNode(n)) {
403
                hits.push(n);
404
            }
405
        }, this);
406
        if (hits.length > 0) {
407
            return hits;
408
        }
409
        // Search up.
410
        hit = node.ancestor('a');
411
        if (hit) {
412
            return [hit];
413
        }
414
        return [];
415
    },
416
 
417
    /**
418
     * Generates the content of the dialogue.
419
     *
420
     * @method _getDialogueContent
421
     * @return {Node} Node containing the dialogue content
422
     * @private
423
     */
424
    _getDialogueContent: function() {
425
        var canShowFilepicker = this.get('host').canShowFilepicker('link'),
426
            template = Y.Handlebars.compile(TEMPLATE);
427
 
428
        this._content = Y.Node.create(template({
429
            showFilepicker: canShowFilepicker,
430
            component: COMPONENTNAME,
431
            CSS: CSS
432
        }));
433
 
434
        this._content.one(SELECTORS.URLINPUT).on('keyup', this._updateTextToDisplay, this);
435
        this._content.one(SELECTORS.URLINPUT).on('change', this._updateTextToDisplay, this);
436
        this._content.one(SELECTORS.URLTEXT).on('keyup', this._setTextToDisplayState, this);
437
        this._content.one(SELECTORS.SUBMIT).on('click', this._setLink, this);
438
        if (canShowFilepicker) {
439
            this._content.one(SELECTORS.LINKBROWSER).on('click', function(e) {
440
                e.preventDefault();
441
                this.get('host').showFilepicker('link', this._filepickerCallback, this);
442
            }, this);
443
        }
444
 
445
        return this._content;
446
    },
447
 
448
    /**
449
     * Unlinks the current selection.
450
     * If the selection is empty (e.g. the cursor is placed within a link),
451
     * then the whole link is unlinked.
452
     *
453
     * @method _unlink
454
     * @private
455
     */
456
    _unlink: function() {
457
        var host = this.get('host'),
458
            range = host.getSelection();
459
 
460
        if (range && range.length) {
461
            if (range[0].startOffset === range[0].endOffset) {
462
                // The cursor was placed in the editor but there was no selection - select the whole parent.
463
                var nodes = host.getSelectedNodes();
464
                if (nodes) {
465
                    // We need to unlink each anchor individually - we cannot select a range because it may only consist of a
466
                    // fragment of an anchor. Selecting the parent would be dangerous because it may contain other links which
467
                    // would then be unlinked too.
468
                    nodes.each(function(node) {
469
                        // We need to select the whole anchor node for this to work in some browsers.
470
                        // We only need to search up because getSelectedNodes returns all Nodes in the selection.
471
                        var anchor = node.ancestor('a', true);
472
                        if (anchor) {
473
                            // Set the selection to the whole of the first anchor.
474
                            host.setSelection(host.getSelectionFromNode(anchor));
475
 
476
                            // Call the browser unlink.
477
                            document.execCommand('unlink', false, null);
478
                        }
479
                    }, this);
480
 
481
                    // And mark the text area as updated.
482
                    this.markUpdated();
483
                }
484
            } else {
485
                // Call the browser unlink.
486
                document.execCommand('unlink', false, null);
487
 
488
                // And mark the text area as updated.
489
                this.markUpdated();
490
            }
491
        }
492
    },
493
 
494
    /**
495
     * Set the current text to display state.
496
     *
497
     * @method _setTextToDisplayState
498
     * @private
499
     */
500
    _setTextToDisplayState: function() {
501
        var urlText,
502
            urlTextVal;
503
        urlText = this._content.one(SELECTORS.URLTEXT);
504
        urlTextVal = urlText.get('value');
505
        if (urlTextVal !== '') {
506
            this._hasTextToDisplay = true;
507
        } else {
508
            this._hasTextToDisplay = false;
509
        }
510
    },
511
 
512
    /**
513
     * Update the text to display if the user does not provide the custom text.
514
     *
515
     * @method _updateTextToDisplay
516
     * @private
517
     */
518
    _updateTextToDisplay: function() {
519
        var urlEntry,
520
            urlText,
521
            urlEntryVal;
522
        urlEntry = this._content.one(SELECTORS.URLINPUT);
523
        urlText = this._content.one(SELECTORS.URLTEXT);
524
        urlEntryVal = urlEntry.get('value');
525
        if (!this._hasTextToDisplay) {
526
            urlText.set('value', urlEntryVal);
527
        }
528
    },
529
 
530
    /**
531
     * Get only the selected text.
532
     * In some cases, window.getSelection() is not run as expected. We should only get the text value
533
     * For ex: <img src="" alt="XYZ">Some text here
534
     *          window.getSelection() will return XYZSome text here
535
     *
536
     * @returns {string} Selected text
537
     * @private
538
     */
539
    _getTextSelection: function() {
540
        var selText = '';
541
        var sel = window.getSelection();
542
        var rangeCount = sel.rangeCount;
543
        if (rangeCount) {
544
            var rangeTexts = [];
545
            for (var i = 0; i < rangeCount; ++i) {
546
                rangeTexts.push('' + sel.getRangeAt(i));
547
            }
548
            selText = rangeTexts.join('');
549
        }
550
        return selText;
551
    }
552
});
553
 
554
 
555
}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});