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