MDL-64506 templates: Move BS2 btns' to BS4 btns'
[moodle.git] / lib / editor / atto / plugins / link / yui / build / moodle-atto_link-button / moodle-atto_link-button.js
1 YUI.add('moodle-atto_link-button', function (Y, NAME) {
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/>.
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  */
24 /**
25  * @module moodle-atto_link-button
26  */
28 /**
29  * Atto text editor link plugin.
30  *
31  * @namespace M.atto_link
32  * @class button
33  * @extends M.editor_atto.EditorPlugin
34  */
36 var COMPONENTNAME = 'atto_link',
37     CSS = {
38         NEWWINDOW: 'atto_link_openinnewwindow',
39         URLINPUT: 'atto_link_urlentry'
40     },
41     SELECTORS = {
42         URLINPUT: '.atto_link_urlentry'
43     },
44     TEMPLATE = '' +
45             '<form class="atto_form">' +
46                 '{{#if showFilepicker}}' +
47                     '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
48                     '<div class="input-group input-append w-100 m-b-1">' +
49                         '<input class="form-control url {{CSS.URLINPUT}}" type="url" ' +
50                         'id="{{elementid}}_atto_link_urlentry"/>' +
51                         '<span class="input-group-append">' +
52                             '<button class="btn btn-secondary openlinkbrowser" type="button">' +
53                             '{{get_string "browserepositories" component}}</button>' +
54                         '</span>' +
55                     '</div>' +
56                 '{{else}}' +
57                     '<div class="m-b-1">' +
58                         '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
59                         '<input class="form-control fullwidth url {{CSS.URLINPUT}}" type="url" ' +
60                         'id="{{elementid}}_atto_link_urlentry" size="32"/>' +
61                     '</div>' +
62                 '{{/if}}' +
63                 '<div class="form-check">' +
64                     '<input type="checkbox" class="form-check-input newwindow" id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
65                     '<label class="form-check-label" for="{{elementid}}_{{CSS.NEWWINDOW}}">' +
66                     '{{get_string "openinnewwindow" component}}' +
67                     '</label>' +
68                 '</div>' +
69                 '<div class="mdl-align">' +
70                     '<br/>' +
71                     '<button type="submit" class="btn btn-secondary submit">{{get_string "createlink" component}}</button>' +
72                 '</div>' +
73             '</form>';
74 Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
76     /**
77      * A reference to the current selection at the time that the dialogue
78      * was opened.
79      *
80      * @property _currentSelection
81      * @type Range
82      * @private
83      */
84     _currentSelection: null,
86     /**
87      * A reference to the dialogue content.
88      *
89      * @property _content
90      * @type Node
91      * @private
92      */
93     _content: null,
95     initializer: function() {
96         // Add the link button first.
97         this.addButton({
98             icon: 'e/insert_edit_link',
99             keys: '75',
100             callback: this._displayDialogue,
101             tags: 'a',
102             tagMatchRequiresAll: false
103         });
105         // And then the unlink button.
106         this.addButton({
107             buttonName: 'unlink',
108             callback: this._unlink,
109             icon: 'e/remove_link',
110             title: 'unlink',
112             // Watch the following tags and add/remove highlighting as appropriate:
113             tags: 'a',
114             tagMatchRequiresAll: false
115         });
116     },
118     /**
119      * Display the link editor.
120      *
121      * @method _displayDialogue
122      * @private
123      */
124     _displayDialogue: function() {
125         // Store the current selection.
126         this._currentSelection = this.get('host').getSelection();
127         if (this._currentSelection === false) {
128             return;
129         }
131         var dialogue = this.getDialogue({
132             headerContent: M.util.get_string('createlink', COMPONENTNAME),
133             width: 'auto',
134             focusAfterHide: true,
135             focusOnShowSelector: SELECTORS.URLINPUT
136         });
138         // Set the dialogue content, and then show the dialogue.
139         dialogue.set('bodyContent', this._getDialogueContent());
141         // Resolve anchors in the selected text.
142         this._resolveAnchors();
143         dialogue.show();
144     },
146     /**
147      * If there is selected text and it is part of an anchor link,
148      * extract the url (and target) from the link (and set them in the form).
149      *
150      * @method _resolveAnchors
151      * @private
152      */
153     _resolveAnchors: function() {
154         // Find the first anchor tag in the selection.
155         var selectednode = this.get('host').getSelectionParentNode(),
156             anchornodes,
157             anchornode,
158             url,
159             target;
161         // Note this is a document fragment and YUI doesn't like them.
162         if (!selectednode) {
163             return;
164         }
166         anchornodes = this._findSelectedAnchors(Y.one(selectednode));
167         if (anchornodes.length > 0) {
168             anchornode = anchornodes[0];
169             this._currentSelection = this.get('host').getSelectionFromNode(anchornode);
170             url = anchornode.getAttribute('href');
171             target = anchornode.getAttribute('target');
172             if (url !== '') {
173                 this._content.one('.url').setAttribute('value', url);
174             }
175             if (target === '_blank') {
176                 this._content.one('.newwindow').setAttribute('checked', 'checked');
177             } else {
178                 this._content.one('.newwindow').removeAttribute('checked');
179             }
180         }
181     },
183     /**
184      * Update the dialogue after a link was selected in the File Picker.
185      *
186      * @method _filepickerCallback
187      * @param {object} params The parameters provided by the filepicker
188      * containing information about the link.
189      * @private
190      */
191     _filepickerCallback: function(params) {
192         this.getDialogue()
193                 .set('focusAfterHide', null)
194                 .hide();
196         if (params.url !== '') {
197             // Add the link.
198             this._setLinkOnSelection(params.url);
200             // And mark the text area as updated.
201             this.markUpdated();
202         }
203     },
205     /**
206      * The link was inserted, so make changes to the editor source.
207      *
208      * @method _setLink
209      * @param {EventFacade} e
210      * @private
211      */
212     _setLink: function(e) {
213         var input,
214             value;
216         e.preventDefault();
217         this.getDialogue({
218             focusAfterHide: null
219         }).hide();
221         input = this._content.one('.url');
223         value = input.get('value');
224         if (value !== '') {
226             // We add a prefix if it is not already prefixed.
227             value = value.trim();
228             var expr = new RegExp(/^[a-zA-Z]*\.*\/|^#|^[a-zA-Z]*:/);
229             if (!expr.test(value)) {
230                 value = 'http://' + value;
231             }
233             // Add the link.
234             this._setLinkOnSelection(value);
236             this.markUpdated();
237         }
238     },
240     /**
241      * Final step setting the anchor on the selection.
242      *
243      * @private
244      * @method _setLinkOnSelection
245      * @param  {String} url URL the link will point to.
246      * @return {Node} The added Node.
247      */
248     _setLinkOnSelection: function(url) {
249         var host = this.get('host'),
250             link,
251             selectednode,
252             target,
253             anchornodes;
255         this.editor.focus();
256         host.setSelection(this._currentSelection);
258         if (this._currentSelection[0].collapsed) {
259             // Firefox cannot add links when the selection is empty so we will add it manually.
260             link = Y.Node.create('<a>' + url + '</a>');
261             link.setAttribute('href', url);
263             // Add the node and select it to replicate the behaviour of execCommand.
264             selectednode = host.insertContentAtFocusPoint(link.get('outerHTML'));
265             host.setSelection(host.getSelectionFromNode(selectednode));
266         } else {
267             document.execCommand('unlink', false, null);
268             document.execCommand('createLink', false, url);
270             // Now set the target.
271             selectednode = host.getSelectionParentNode();
272         }
274         // Note this is a document fragment and YUI doesn't like them.
275         if (!selectednode) {
276             return;
277         }
279         anchornodes = this._findSelectedAnchors(Y.one(selectednode));
280         // Add new window attributes if requested.
281         Y.Array.each(anchornodes, function(anchornode) {
282             target = this._content.one('.newwindow');
283             if (target.get('checked')) {
284                 anchornode.setAttribute('target', '_blank');
285             } else {
286                 anchornode.removeAttribute('target');
287             }
288         }, this);
290         return selectednode;
291     },
293     /**
294      * Look up and down for the nearest anchor tags that are least partly contained in the selection.
295      *
296      * @method _findSelectedAnchors
297      * @param {Node} node The node to search under for the selected anchor.
298      * @return {Node|Boolean} The Node, or false if not found.
299      * @private
300      */
301     _findSelectedAnchors: function(node) {
302         var tagname = node.get('tagName'),
303             hit, hits;
305         // Direct hit.
306         if (tagname && tagname.toLowerCase() === 'a') {
307             return [node];
308         }
310         // Search down but check that each node is part of the selection.
311         hits = [];
312         node.all('a').each(function(n) {
313             if (!hit && this.get('host').selectionContainsNode(n)) {
314                 hits.push(n);
315             }
316         }, this);
317         if (hits.length > 0) {
318             return hits;
319         }
320         // Search up.
321         hit = node.ancestor('a');
322         if (hit) {
323             return [hit];
324         }
325         return [];
326     },
328     /**
329      * Generates the content of the dialogue.
330      *
331      * @method _getDialogueContent
332      * @return {Node} Node containing the dialogue content
333      * @private
334      */
335     _getDialogueContent: function() {
336         var canShowFilepicker = this.get('host').canShowFilepicker('link'),
337             template = Y.Handlebars.compile(TEMPLATE);
339         this._content = Y.Node.create(template({
340             showFilepicker: canShowFilepicker,
341             component: COMPONENTNAME,
342             CSS: CSS
343         }));
345         this._content.one('.submit').on('click', this._setLink, this);
346         if (canShowFilepicker) {
347             this._content.one('.openlinkbrowser').on('click', function(e) {
348                 e.preventDefault();
349                 this.get('host').showFilepicker('link', this._filepickerCallback, this);
350             }, this);
351         }
353         return this._content;
354     },
356     /**
357      * Unlinks the current selection.
358      * If the selection is empty (e.g. the cursor is placed within a link),
359      * then the whole link is unlinked.
360      *
361      * @method _unlink
362      * @private
363      */
364     _unlink: function() {
365         var host = this.get('host'),
366             range = host.getSelection();
368         if (range && range.length) {
369             if (range[0].startOffset === range[0].endOffset) {
370                 // The cursor was placed in the editor but there was no selection - select the whole parent.
371                 var nodes = host.getSelectedNodes();
372                 if (nodes) {
373                     // We need to unlink each anchor individually - we cannot select a range because it may only consist of a
374                     // fragment of an anchor. Selecting the parent would be dangerous because it may contain other links which
375                     // would then be unlinked too.
376                     nodes.each(function(node) {
377                         // We need to select the whole anchor node for this to work in some browsers.
378                         // We only need to search up because getSeletedNodes returns all Nodes in the selection.
379                         var anchor = node.ancestor('a', true);
380                         if (anchor) {
381                             // Set the selection to the whole of the first anchro.
382                             host.setSelection(host.getSelectionFromNode(anchor));
384                             // Call the browser unlink.
385                             document.execCommand('unlink', false, null);
386                         }
387                     }, this);
389                     // And mark the text area as updated.
390                     this.markUpdated();
391                 }
392             } else {
393                 // Call the browser unlink.
394                 document.execCommand('unlink', false, null);
396                 // And mark the text area as updated.
397                 this.markUpdated();
398             }
399         }
400     }
401 });
404 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});