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