Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
 * Helper for Tiny noautolink plugin.
18
 *
19
 * @module      tiny_noautolink/noautolink
20
 * @copyright   2023 Meirza <meirza.arson@moodle.com>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import Pending from 'core/pending';
25
 
26
const noautolinkClassName = 'nolink';
27
const noautolinkTagHTML = 'span';
28
const notificationTimeout = 2000;
29
 
30
/**
31
 * Handle action.
32
 *
33
 * @param {TinyMCE} editor
34
 * @param {object} messages
35
 */
36
export const handleAction = (editor, messages) => {
37
    const toggleState = isInAnchor(editor, editor.selection.getNode());
38
    const urlString = getSelectedContent(editor);
39
    if (!toggleState && urlString !== '') {
40
        setNoAutoLink(editor, messages, urlString);
41
    } else if (toggleState) {
42
        unsetNoAutoLink(editor, messages, urlString);
43
    } else {
44
        editor.notificationManager.open({text: messages.infoEmptySelection, type: 'info', timeout: notificationTimeout});
45
    }
46
};
47
 
48
/**
49
 * Display notification feedback when applying the noautolink to the selected text.
50
 *
51
 * @param {TinyMCE} editor
52
 * @param {object} messages
53
 * @param {String} urlString
54
 */
55
const setNoAutoLink = (editor, messages, urlString) => {
56
    // Check whether the string is a URL. Otherwise, show an error notification.
57
    if (isValidUrl(urlString)) {
58
        const pendingPromise = new Pending('tiny_noautolink/setNoautolink');
59
        // Applying the auto-link prevention.
60
        setNoautolinkOnSelection(editor, urlString)
61
        .catch(error => {
62
            editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout});
63
        })
64
        .finally(() => {
65
            editor.notificationManager.open({text: messages.infoAddSuccess, type: 'success', timeout: notificationTimeout});
66
            pendingPromise.resolve();
67
        });
68
    } else {
69
        editor.notificationManager.open({text: messages.errorInvalidURL, type: 'error', timeout: notificationTimeout});
70
    }
71
};
72
 
73
/**
74
 * Display notification feedback when removing the noautolink to the selected text.
75
 *
76
 * @param {TinyMCE} editor
77
 * @param {object} messages
78
 */
79
const unsetNoAutoLink = (editor, messages) => {
80
    const nodeString = editor.selection.getNode().outerHTML.trim();
81
    // Convert HTML string to DOM element to get nolink class.
82
    const wrapper = document.createElement('div');
83
    wrapper.innerHTML = nodeString;
84
    const tempElement = wrapper.firstChild;
85
    if (tempElement.classList.contains('nolink')) {
86
        const pendingPromise = new Pending('tiny_noautolink/setNoautolink');
87
        // Removing the auto-link prevention.
88
        unsetNoautolinkOnSelection(editor, nodeString)
89
        .catch(error => {
90
            editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout});
91
            pendingPromise.reject(error); // Handle the error as needed.
92
        })
93
        .finally(() => {
94
            editor.notificationManager.open({text: messages.infoRemoveSuccess, type: 'success', timeout: notificationTimeout});
95
            pendingPromise.resolve();
96
        });
97
    }
98
};
99
 
100
/**
101
 * Return the full string based on the position of the cursor within the string.
102
 *
103
 * @param {TinyMCE} editor
104
 * @returns {String}
105
 */
106
const getSelectedContent = (editor) => {
107
    const selection = editor.selection; // Get the selection object.
108
    let content = selection.getContent({format: 'text'}).trim();
109
    if (content == '') {
110
        const range = selection.getRng(); // Get the range object.
111
 
112
        // Check if the cursor is within a text node.
113
        if (range.startContainer.nodeType === Node.TEXT_NODE) {
114
            const textContent = range.startContainer.textContent;
115
            const cursorOffset = range.startOffset;
116
 
117
            // Find the word boundaries around the cursor.
118
            let wordStart = cursorOffset;
119
            while (wordStart > 0 && /\S/.test(textContent[wordStart - 1])) {
120
                wordStart--;
121
            }
122
 
123
            let wordEnd = cursorOffset;
124
            while (wordEnd < textContent.length && /\S/.test(textContent[wordEnd])) {
125
                wordEnd++;
126
            }
127
 
128
            // Set the selection range to the word.
129
            selection.setRng({
130
                startContainer: range.startContainer,
131
                startOffset: wordStart,
132
                endContainer: range.startContainer,
133
                endOffset: wordEnd,
134
            });
135
            content = selection.getContent({format: 'text'}).trim();
136
        }
137
    }
138
    return content;
139
};
140
 
141
/**
142
 * Wrap the selection with the nolink class.
143
 *
144
 * @param {TinyMCE} editor
145
 * @param {String} url URL the link will point to.
146
 */
147
const setNoautolinkOnSelection = async(editor, url) => {
148
    const newContent = `<${noautolinkTagHTML} class="${noautolinkClassName}">${url}</${noautolinkTagHTML}>`;
149
    editor.selection.setContent(newContent);
150
 
151
    // Select the new content.
152
    const currentNode = editor.selection.getNode();
153
    const currentDOM = editor.dom.select(`${noautolinkTagHTML}.${noautolinkClassName}`, currentNode);
154
    currentDOM.forEach(function(value, index) {
155
        if (value.outerHTML == newContent) {
156
            editor.selection.select(currentDOM[index]);
157
            return;
158
        }
159
    });
160
};
161
 
162
/**
163
 * Remove the nolink on the selection.
164
 *
165
 * @param {TinyMCE} editor
166
 * @param {String} url URL the link will point to.
167
 */
168
const unsetNoautolinkOnSelection = async(editor, url) => {
169
    const regex = new RegExp(`</?${noautolinkTagHTML}[^>]*>`, "g");
170
    url = url.replace(regex, "");
171
    const currentSpan = editor.dom.getParent(editor.selection.getNode(), noautolinkTagHTML);
172
    currentSpan.outerHTML = url;
173
};
174
 
175
/**
176
 * Check if given string is a valid URL.
177
 *
178
 * @param {String} urlString URL the link will point to.
179
 * @returns {boolean} True is valid, otherwise false.
180
 */
181
const isValidUrl = urlString => {
182
    const urlPattern = new RegExp('^((http|https):\\/\\/|www\\.)' + // A URL must have one of these https/https/www.
183
                                '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Validate domain name.
184
                                '((\\d{1,3}\\.){3}\\d{1,3}))' + // Validate ip (v4) address.
185
                                '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // Validate port and path.
186
                                '(\\?[;&a-z\\d%_.~+=-]*)?' + // Validate query string.
187
                                '(\\#[-a-z\\d_]*)?$', 'i'); // Validate fragment locator.
188
 
189
    return !!urlPattern.test(urlString);
190
};
191
 
192
/**
193
 * Get anchor element.
194
 *
195
 * @param {TinyMCE} editor
196
 * @param {Element} selectedElm
197
 * @returns {Element}
198
 */
199
const getAnchorElement = (editor, selectedElm) => {
200
    selectedElm = selectedElm || editor.selection.getNode();
201
    return editor.dom.getParent(selectedElm, `${noautolinkTagHTML}.${noautolinkClassName}`);
202
};
203
 
204
 
205
/**
206
 * Check the current selected element is an anchor or not.
207
 *
208
 * @param {TinyMCE} editor
209
 * @param {Element} selectedElm
210
 * @returns {boolean}
211
 */
212
const isInAnchor = (editor, selectedElm) => getAnchorElement(editor, selectedElm) !== null;
213
 
214
/**
215
 * Change state of button.
216
 *
217
 * @param {TinyMCE} editor
218
 * @param {function()} toggler
219
 * @returns {function()}
220
 */
221
const toggleState = (editor, toggler) => {
222
    editor.on('NodeChange', toggler);
223
    return () => editor.off('NodeChange', toggler);
224
};
225
 
226
/**
227
 * Change the active state of button.
228
 *
229
 * @param {TinyMCE} editor
230
 * @returns {function(*): function(): *}
231
 */
232
export const toggleActiveState = (editor) => (api) => {
233
    const updateState = () => api.setActive(!editor.mode.isReadOnly() && isInAnchor(editor, editor.selection.getNode()));
234
    updateState();
235
    return toggleState(editor, updateState);
236
};