1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 * @module moodle-atto_link-button
27 * Atto text editor link plugin.
29 * @namespace M.atto_link
31 * @extends M.editor_atto.EditorPlugin
34 var COMPONENTNAME = 'atto_link',
36 NEWWINDOW: 'atto_link_openinnewwindow',
37 URLINPUT: 'atto_link_urlentry'
40 URLINPUT: '.atto_link_urlentry'
43 '<form class="atto_form">' +
44 '{{#if showFilepicker}}' +
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">' +
50 '<button class="btn btn-default openlinkbrowser" type="button">' +
51 '{{get_string "browserepositories" component}}</button>' +
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"/>' +
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}}' +
67 '<div class="mdl-align">' +
69 '<button type="submit" class="btn btn-default submit">{{get_string "createlink" component}}</button>' +
72 Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
75 * A reference to the current selection at the time that the dialogue
78 * @property _currentSelection
82 _currentSelection: null,
85 * A reference to the dialogue content.
93 initializer: function() {
94 // Add the link button first.
96 icon: 'e/insert_edit_link',
98 callback: this._displayDialogue,
100 tagMatchRequiresAll: false
103 // And then the unlink button.
105 buttonName: 'unlink',
106 callback: this._unlink,
107 icon: 'e/remove_link',
110 // Watch the following tags and add/remove highlighting as appropriate:
112 tagMatchRequiresAll: false
117 * Display the link editor.
119 * @method _displayDialogue
122 _displayDialogue: function() {
123 // Store the current selection.
124 this._currentSelection = this.get('host').getSelection();
125 if (this._currentSelection === false) {
129 var dialogue = this.getDialogue({
130 headerContent: M.util.get_string('createlink', COMPONENTNAME),
132 focusAfterHide: true,
133 focusOnShowSelector: SELECTORS.URLINPUT
136 // Set the dialogue content, and then show the dialogue.
137 dialogue.set('bodyContent', this._getDialogueContent());
139 // Resolve anchors in the selected text.
140 this._resolveAnchors();
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).
148 * @method _resolveAnchors
151 _resolveAnchors: function() {
152 // Find the first anchor tag in the selection.
153 var selectednode = this.get('host').getSelectionParentNode(),
159 // Note this is a document fragment and YUI doesn't like them.
164 anchornodes = this._findSelectedAnchors(Y.one(selectednode));
165 if (anchornodes.length > 0) {
166 anchornode = anchornodes[0];
167 this._currentSelection = this.get('host').getSelectionFromNode(anchornode);
168 url = anchornode.getAttribute('href');
169 target = anchornode.getAttribute('target');
171 this._content.one('.url').setAttribute('value', url);
173 if (target === '_blank') {
174 this._content.one('.newwindow').setAttribute('checked', 'checked');
176 this._content.one('.newwindow').removeAttribute('checked');
182 * Update the dialogue after a link was selected in the File Picker.
184 * @method _filepickerCallback
185 * @param {object} params The parameters provided by the filepicker
186 * containing information about the link.
189 _filepickerCallback: function(params) {
191 .set('focusAfterHide', null)
194 if (params.url !== '') {
196 this._setLinkOnSelection(params.url);
198 // And mark the text area as updated.
204 * The link was inserted, so make changes to the editor source.
207 * @param {EventFacade} e
210 _setLink: function(e) {
219 input = this._content.one('.url');
221 value = input.get('value');
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;
232 this._setLinkOnSelection(value);
239 * Final step setting the anchor on the selection.
242 * @method _setLinkOnSelection
243 * @param {String} url URL the link will point to.
244 * @return {Node} The added Node.
246 _setLinkOnSelection: function(url) {
247 var host = this.get('host'),
254 host.setSelection(this._currentSelection);
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);
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));
265 document.execCommand('unlink', false, null);
266 document.execCommand('createLink', false, url);
268 // Now set the target.
269 selectednode = host.getSelectionParentNode();
272 // Note this is a document fragment and YUI doesn't like them.
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');
284 anchornode.removeAttribute('target');
292 * Look up and down for the nearest anchor tags that are least partly contained in the selection.
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.
299 _findSelectedAnchors: function(node) {
300 var tagname = node.get('tagName'),
304 if (tagname && tagname.toLowerCase() === 'a') {
308 // Search down but check that each node is part of the selection.
310 node.all('a').each(function(n) {
311 if (!hit && this.get('host').selectionContainsNode(n)) {
315 if (hits.length > 0) {
319 hit = node.ancestor('a');
327 * Generates the content of the dialogue.
329 * @method _getDialogueContent
330 * @return {Node} Node containing the dialogue content
333 _getDialogueContent: function() {
334 var canShowFilepicker = this.get('host').canShowFilepicker('link'),
335 template = Y.Handlebars.compile(TEMPLATE);
337 this._content = Y.Node.create(template({
338 showFilepicker: canShowFilepicker,
339 component: COMPONENTNAME,
343 this._content.one('.submit').on('click', this._setLink, this);
344 if (canShowFilepicker) {
345 this._content.one('.openlinkbrowser').on('click', function(e) {
347 this.get('host').showFilepicker('link', this._filepickerCallback, this);
351 return this._content;
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.
362 _unlink: function() {
363 var host = this.get('host'),
364 range = host.getSelection();
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();
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);
379 // Set the selection to the whole of the first anchro.
380 host.setSelection(host.getSelectionFromNode(anchor));
382 // Call the browser unlink.
383 document.execCommand('unlink', false, null);
387 // And mark the text area as updated.
391 // Call the browser unlink.
392 document.execCommand('unlink', false, null);
394 // And mark the text area as updated.