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-debug.js
CommitLineData
adca7326
DW
1YUI.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
62467795
AN
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
3ee53a42 24/**
62467795 25 * @module moodle-atto_link-button
3ee53a42 26 */
3ee53a42 27
adca7326
DW
28/**
29 * Atto text editor link plugin.
30 *
62467795
AN
31 * @namespace M.atto_link
32 * @class button
33 * @extends M.editor_atto.EditorPlugin
adca7326 34 */
62467795
AN
35
36var COMPONENTNAME = 'atto_link',
37 CSS = {
e5ddec38
DW
38 NEWWINDOW: 'atto_link_openinnewwindow',
39 URLINPUT: 'atto_link_urlentry'
40 },
41 SELECTORS = {
42 URLINPUT: '.atto_link_urlentry'
62467795
AN
43 },
44 TEMPLATE = '' +
45 '<form class="atto_form">' +
62467795 46 '{{#if showFilepicker}}' +
1b217025
BB
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">' +
29551c4b 52 '<button class="btn btn-secondary openlinkbrowser" type="button">' +
1b217025
BB
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>' +
62467795 62 '{{/if}}' +
1b217025
BB
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>' +
62467795
AN
69 '<div class="mdl-align">' +
70 '<br/>' +
29551c4b 71 '<button type="submit" class="btn btn-secondary submit">{{get_string "createlink" component}}</button>' +
62467795
AN
72 '</div>' +
73 '</form>';
74Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
adca7326
DW
75
76 /**
62467795
AN
77 * A reference to the current selection at the time that the dialogue
78 * was opened.
adca7326 79 *
62467795 80 * @property _currentSelection
adca7326 81 * @type Range
62467795 82 * @private
adca7326 83 */
62467795 84 _currentSelection: null,
adca7326
DW
85
86 /**
62467795 87 * A reference to the dialogue content.
adca7326 88 *
62467795
AN
89 * @property _content
90 * @type Node
91 * @private
adca7326 92 */
62467795 93 _content: null,
adca7326 94
62467795 95 initializer: function() {
49a510ef 96 // Add the link button first.
62467795
AN
97 this.addButton({
98 icon: 'e/insert_edit_link',
36231b6c 99 keys: '75',
62467795 100 callback: this._displayDialogue,
664c8517
FM
101 tags: 'a',
102 tagMatchRequiresAll: false
62467795 103 });
49a510ef
AN
104
105 // And then the unlink button.
64115135
AN
106 this.addButton({
107 buttonName: 'unlink',
108 callback: this._unlink,
49a510ef
AN
109 icon: 'e/remove_link',
110 title: 'unlink',
111
112 // Watch the following tags and add/remove highlighting as appropriate:
664c8517
FM
113 tags: 'a',
114 tagMatchRequiresAll: false
49a510ef 115 });
adca7326
DW
116 },
117
118 /**
62467795 119 * Display the link editor.
adca7326 120 *
62467795
AN
121 * @method _displayDialogue
122 * @private
adca7326 123 */
62467795
AN
124 _displayDialogue: function() {
125 // Store the current selection.
126 this._currentSelection = this.get('host').getSelection();
39c6f62d 127 if (this._currentSelection === false) {
62467795
AN
128 return;
129 }
130
131 var dialogue = this.getDialogue({
132 headerContent: M.util.get_string('createlink', COMPONENTNAME),
104cc0e3 133 width: 'auto',
e5ddec38 134 focusAfterHide: true,
c1660772 135 focusOnShowSelector: SELECTORS.URLINPUT
3ee53a42 136 });
62467795
AN
137
138 // Set the dialogue content, and then show the dialogue.
139 dialogue.set('bodyContent', this._getDialogueContent());
140
141 // Resolve anchors in the selected text.
142 this._resolveAnchors();
143 dialogue.show();
adca7326
DW
144 },
145
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 *
62467795
AN
150 * @method _resolveAnchors
151 * @private
adca7326 152 */
62467795 153 _resolveAnchors: function() {
adca7326 154 // Find the first anchor tag in the selection.
62467795 155 var selectednode = this.get('host').getSelectionParentNode(),
adca7326
DW
156 anchornodes,
157 anchornode,
62467795
AN
158 url,
159 target;
adca7326
DW
160
161 // Note this is a document fragment and YUI doesn't like them.
162 if (!selectednode) {
163 return;
164 }
165
62467795 166 anchornodes = this._findSelectedAnchors(Y.one(selectednode));
adca7326
DW
167 if (anchornodes.length > 0) {
168 anchornode = anchornodes[0];
62467795 169 this._currentSelection = this.get('host').getSelectionFromNode(anchornode);
adca7326
DW
170 url = anchornode.getAttribute('href');
171 target = anchornode.getAttribute('target');
172 if (url !== '') {
62467795 173 this._content.one('.url').setAttribute('value', url);
adca7326
DW
174 }
175 if (target === '_blank') {
62467795 176 this._content.one('.newwindow').setAttribute('checked', 'checked');
adca7326 177 } else {
62467795 178 this._content.one('.newwindow').removeAttribute('checked');
adca7326
DW
179 }
180 }
181 },
182
183 /**
39c6f62d 184 * Update the dialogue after a link was selected in the File Picker.
adca7326 185 *
62467795
AN
186 * @method _filepickerCallback
187 * @param {object} params The parameters provided by the filepicker
39c6f62d 188 * containing information about the link.
62467795 189 * @private
adca7326 190 */
62467795
AN
191 _filepickerCallback: function(params) {
192 this.getDialogue()
193 .set('focusAfterHide', null)
194 .hide();
adca7326 195
adca7326 196 if (params.url !== '') {
39c6f62d
FM
197 // Add the link.
198 this._setLinkOnSelection(params.url);
199
457a9fa6
DW
200 // And mark the text area as updated.
201 this.markUpdated();
adca7326
DW
202 }
203 },
204
205 /**
62467795 206 * The link was inserted, so make changes to the editor source.
adca7326 207 *
62467795
AN
208 * @method _setLink
209 * @param {EventFacade} e
210 * @private
adca7326 211 */
62467795 212 _setLink: function(e) {
adca7326 213 var input,
adca7326
DW
214 value;
215
216 e.preventDefault();
62467795
AN
217 this.getDialogue({
218 focusAfterHide: null
219 }).hide();
adca7326 220
62467795 221 input = this._content.one('.url');
adca7326
DW
222
223 value = input.get('value');
224 if (value !== '') {
4879ea0e
DM
225
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 }
232
39c6f62d 233 // Add the link.
cc75779f 234 this._setLinkOnSelection(value);
62467795
AN
235
236 this.markUpdated();
adca7326
DW
237 }
238 },
239
39c6f62d
FM
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,
cc75779f
DP
251 selectednode,
252 target,
253 anchornodes;
39c6f62d
FM
254
255 this.editor.focus();
256 host.setSelection(this._currentSelection);
257
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);
262
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);
269
270 // Now set the target.
271 selectednode = host.getSelectionParentNode();
272 }
273
cc75779f
DP
274 // Note this is a document fragment and YUI doesn't like them.
275 if (!selectednode) {
276 return;
277 }
278
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);
289
39c6f62d
FM
290 return selectednode;
291 },
292
adca7326
DW
293 /**
294 * Look up and down for the nearest anchor tags that are least partly contained in the selection.
295 *
62467795
AN
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
adca7326 300 */
62467795
AN
301 _findSelectedAnchors: function(node) {
302 var tagname = node.get('tagName'),
303 hit, hits;
304
adca7326
DW
305 // Direct hit.
306 if (tagname && tagname.toLowerCase() === 'a') {
307 return [node];
308 }
62467795 309
adca7326
DW
310 // Search down but check that each node is part of the selection.
311 hits = [];
312 node.all('a').each(function(n) {
62467795 313 if (!hit && this.get('host').selectionContainsNode(n)) {
adca7326
DW
314 hits.push(n);
315 }
16c6440d 316 }, this);
adca7326
DW
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 },
327
328 /**
62467795 329 * Generates the content of the dialogue.
adca7326 330 *
62467795
AN
331 * @method _getDialogueContent
332 * @return {Node} Node containing the dialogue content
333 * @private
adca7326 334 */
62467795
AN
335 _getDialogueContent: function() {
336 var canShowFilepicker = this.get('host').canShowFilepicker('link'),
337 template = Y.Handlebars.compile(TEMPLATE);
b269f635 338
62467795
AN
339 this._content = Y.Node.create(template({
340 showFilepicker: canShowFilepicker,
341 component: COMPONENTNAME,
342 CSS: CSS
343 }));
adca7326 344
62467795
AN
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);
b269f635 351 }
62467795
AN
352
353 return this._content;
64115135
AN
354 },
355
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();
367
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));
383
384 // Call the browser unlink.
385 document.execCommand('unlink', false, null);
386 }
387 }, this);
388
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);
395
396 // And mark the text area as updated.
397 this.markUpdated();
398 }
399 }
adca7326 400 }
62467795 401});
adca7326
DW
402
403
62467795 404}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});