Proyectos de Subversion Moodle

Rev

| 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
 * AJAX helper for the inline editing a value.
18
 *
19
 * This script is automatically included from template core/inplace_editable
20
 * It registers a click-listener on [data-inplaceeditablelink] link (the "inplace edit" icon),
21
 * then replaces the displayed value with an input field. On "Enter" it sends a request
22
 * to web service core_update_inplace_editable, which invokes the specified callback.
23
 * Any exception thrown by the web service (or callback) is displayed as an error popup.
24
 *
25
 * @module     core/inplace_editable
26
 * @copyright  2016 Marina Glancy
27
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
 * @since      3.1
29
 */
30
define(
31
    ['jquery',
32
        'core/ajax',
33
        'core/templates',
34
        'core/notification',
35
        'core/str',
36
        'core/config',
37
        'core/url',
38
        'core/form-autocomplete',
39
        'core/pending',
40
        'core/local/inplace_editable/events',
41
    ],
42
    function($, ajax, templates, notification, str, cfg, url, autocomplete, Pending, Events) {
43
 
44
        const removeSpinner = function(element) {
45
            element.removeClass('updating');
46
            element.find('img.spinner').hide();
47
        };
48
 
49
        /**
50
         * Update an inplace editable value.
51
         *
52
         * @param {Jquery} mainelement the element to update
53
         * @param {string} value the new value
54
         * @param {bool} silent if true the change won't alter the current page focus
55
         * @fires event:core/inplace_editable:updated
56
         * @fires event:core/inplace_editable:updateFailed
57
         */
58
        const updateValue = function(mainelement, value, silent) {
59
            var pendingId = [
60
                mainelement.attr('data-itemid'),
61
                mainelement.attr('data-component'),
62
                mainelement.attr('data-itemtype'),
63
            ].join('-');
64
            var pendingPromise = new Pending(pendingId);
65
 
66
            addSpinner(mainelement);
67
            ajax.call([{
68
                methodname: 'core_update_inplace_editable',
69
                args: {
70
                    itemid: mainelement.attr('data-itemid'),
71
                    component: mainelement.attr('data-component'),
72
                    itemtype: mainelement.attr('data-itemtype'),
73
                    value: value,
74
                },
75
            }])[0]
76
                .then(function(data) {
77
                    return templates.render('core/inplace_editable', data)
78
                        .then(function(html, js) {
79
                            var oldvalue = mainelement.attr('data-value');
80
                            var newelement = $(html);
81
                            templates.replaceNode(mainelement, newelement, js);
82
                            if (!silent) {
83
                                newelement.find('[data-inplaceeditablelink]').focus();
84
                            }
85
 
86
                            // Trigger updated event on the DOM element.
87
                            Events.notifyElementUpdated(newelement.get(0), data, oldvalue);
88
 
89
                            return;
90
                        });
91
                })
92
                .then(function() {
93
                    return pendingPromise.resolve();
94
                })
95
                .fail(function(ex) {
96
                    removeSpinner(mainelement);
97
                    M.util.js_complete(pendingId);
98
 
99
                    // Trigger update failed event on the DOM element.
100
                    let updateFailedEvent = Events.notifyElementUpdateFailed(mainelement.get(0), ex, value);
101
                    if (!updateFailedEvent.defaultPrevented) {
102
                        notification.exception(ex);
103
                    }
104
                });
105
        };
106
 
107
        const addSpinner = function(element) {
108
            element.addClass('updating');
109
            var spinner = element.find('img.spinner');
110
            if (spinner.length) {
111
                spinner.show();
112
            } else {
113
                spinner = $('<img/>')
114
                    .attr('src', url.imageUrl('i/loading_small'))
115
                    .addClass('spinner').addClass('smallicon')
116
                    ;
117
                element.append(spinner);
118
            }
119
        };
120
 
121
        $('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
122
            if (e.type === 'keypress' && e.keyCode !== 13) {
123
                return;
124
            }
125
            var editingEnabledPromise = new Pending('autocomplete-start-editing');
126
            e.stopImmediatePropagation();
127
            e.preventDefault();
128
            var target = $(this),
129
                mainelement = target.closest('[data-inplaceeditable]');
130
 
131
            var turnEditingOff = function(el) {
132
                el.find('input').off();
133
                el.find('select').off();
134
                el.html(el.attr('data-oldcontent'));
135
                el.removeAttr('data-oldcontent');
136
                el.removeClass('inplaceeditingon');
137
                el.find('[data-inplaceeditablelink]').focus();
138
 
139
                // Re-enable any parent draggable attribute.
140
                el.parents(`[data-inplace-in-draggable="true"]`)
141
                    .attr('draggable', true)
142
                    .attr('data-inplace-in-draggable', false);
143
            };
144
 
145
            var turnEditingOffEverywhere = function() {
146
                // Re-enable any disabled draggable attribute.
147
                $(`[data-inplace-in-draggable="true"]`)
148
                    .attr('draggable', true)
149
                    .attr('data-inplace-in-draggable', false);
150
 
151
                $('span.inplaceeditable.inplaceeditingon').each(function() {
152
                    turnEditingOff($(this));
153
                });
154
            };
155
 
156
            var uniqueId = function(prefix, idlength) {
157
                var uniqid = prefix,
158
                    i;
159
                for (i = 0; i < idlength; i++) {
160
                    uniqid += String(Math.floor(Math.random() * 10));
161
                }
162
                // Make sure this ID is not already taken by an existing element.
163
                if ($("#" + uniqid).length === 0) {
164
                    return uniqid;
165
                }
166
                return uniqueId(prefix, idlength);
167
            };
168
 
169
            var turnEditingOnText = function(el) {
170
                str.get_string('edittitleinstructions').done(function(s) {
171
                    var instr = $('<span class="editinstructions">' + s + '</span>').
172
                        attr('id', uniqueId('id_editinstructions_', 20)),
173
                        inputelement = $('<input type="text"/>').
174
                            attr('id', uniqueId('id_inplacevalue_', 20)).
175
                            attr('value', el.attr('data-value')).
176
                            attr('aria-describedby', instr.attr('id')).
177
                            addClass('ignoredirty').
178
                            addClass('form-control'),
179
                        lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>').
180
                            attr('for', inputelement.attr('id'));
181
                    el.html('').append(instr).append(lbl).append(inputelement);
182
 
183
                    inputelement.focus();
184
                    inputelement.select();
185
                    inputelement.on('keyup keypress focusout', function(e) {
186
                        if (cfg.behatsiterunning && e.type === 'focusout') {
187
                            // Behat triggers focusout too often.
188
                            return;
189
                        }
190
                        if (e.type === 'keypress' && e.keyCode === 13) {
191
                            // We need 'keypress' event for Enter because keyup/keydown would catch Enter that was
192
                            // pressed in other fields.
193
                            var val = inputelement.val();
194
                            turnEditingOff(el);
195
                            updateValue(el, val);
196
                        }
197
                        if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
198
                            // We need 'keyup' event for Escape because keypress does not work with Escape.
199
                            turnEditingOff(el);
200
                        }
201
                    });
202
                });
203
            };
204
 
205
            var turnEditingOnToggle = function(el, newvalue) {
206
                turnEditingOff(el);
207
                updateValue(el, newvalue);
208
            };
209
 
210
            var turnEditingOnSelect = function(el, options) {
211
                var i,
212
                    inputelement = $('<select></select>').
213
                        attr('id', uniqueId('id_inplacevalue_', 20)).
214
                        addClass('custom-select'),
215
                    lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>')
216
                        .attr('for', inputelement.attr('id'));
217
                for (i in options) {
218
                    inputelement
219
                        .append($('<option>')
220
                            .attr('value', options[i].key)
221
                            .html(options[i].value));
222
                }
223
                inputelement.val(el.attr('data-value'));
224
 
225
                el.html('')
226
                    .append(lbl)
227
                    .append(inputelement);
228
 
229
                inputelement.focus();
230
                inputelement.select();
231
                inputelement.on('keyup change focusout', function(e) {
232
                    if (cfg.behatsiterunning && e.type === 'focusout') {
233
                        // Behat triggers focusout too often.
234
                        return;
235
                    }
236
                    if (e.type === 'change') {
237
                        var val = inputelement.val();
238
                        turnEditingOff(el);
239
                        updateValue(el, val);
240
                    }
241
                    if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
242
                        // We need 'keyup' event for Escape because keypress does not work with Escape.
243
                        turnEditingOff(el);
244
                    }
245
                });
246
            };
247
 
248
            var turnEditingOnAutocomplete = function(el, args) {
249
                var i,
250
                    inputelement = $('<select></select>').
251
                        attr('id', uniqueId('id_inplacevalue_', 20)).
252
                        addClass('form-autocomplete-original-select').
253
                        addClass('custom-select'),
254
                    lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>')
255
                        .attr('for', inputelement.attr('id')),
256
                    options = args.options,
257
                    attributes = args.attributes,
258
                    saveelement = $('<a href="#"></a>'),
259
                    cancelelement = $('<a href="#"></a>');
260
 
261
                for (i in options) {
262
                    inputelement
263
                        .append($('<option>')
264
                            .attr('value', options[i].key)
265
                            .html(options[i].value));
266
                }
267
                if (attributes.multiple) {
268
                    inputelement.attr('multiple', 'true');
269
                }
270
                inputelement.val(JSON.parse(el.attr('data-value')));
271
 
272
                str.get_string('savechanges', 'core').then(function(s) {
273
                    return templates.renderPix('e/save', 'core', s);
274
                }).then(function(html) {
275
                    saveelement.append(html);
276
                    return;
277
                }).fail(notification.exception);
278
 
279
                str.get_string('cancel', 'core').then(function(s) {
280
                    return templates.renderPix('e/cancel', 'core', s);
281
                }).then(function(html) {
282
                    cancelelement.append(html);
283
                    return;
284
                }).fail(notification.exception);
285
 
286
                el.html('')
287
                    .append(lbl)
288
                    .append(inputelement)
289
                    .append(saveelement)
290
                    .append(cancelelement);
291
 
292
                inputelement.focus();
293
                inputelement.select();
294
                autocomplete.enhance(inputelement,
295
                    attributes.tags,
296
                    attributes.ajax,
297
                    attributes.placeholder,
298
                    attributes.caseSensitive,
299
                    attributes.showSuggestions,
300
                    attributes.noSelectionString)
301
                    .then(function() {
302
                        // Focus on the enhanced combobox.
303
                        el.find('[role=combobox]').focus();
304
                        // Stop eslint nagging.
305
                        return;
306
                    }).fail(notification.exception);
307
 
308
                inputelement.on('keyup', function(e) {
309
                    if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
310
                        // We need 'keyup' event for Escape because keypress does not work with Escape.
311
                        turnEditingOff(el);
312
                    }
313
                });
314
                saveelement.on('click', function(e) {
315
                    var val = JSON.stringify(inputelement.val());
316
                    // We need to empty the node to destroy all event handlers etc.
317
                    inputelement.empty();
318
                    turnEditingOff(el);
319
                    updateValue(el, val);
320
                    e.preventDefault();
321
                });
322
                cancelelement.on('click', function(e) {
323
                    // We need to empty the node to destroy all event handlers etc.
324
                    inputelement.empty();
325
                    turnEditingOff(el);
326
                    e.preventDefault();
327
                });
328
            };
329
 
330
            var turnEditingOn = function(el) {
331
                el.addClass('inplaceeditingon');
332
                el.attr('data-oldcontent', el.html());
333
 
334
                var type = el.attr('data-type');
335
                var options = el.attr('data-options');
336
 
337
                // Input text inside draggable elements disable text selection in some browsers.
338
                // To prevent this we temporally disable any parent draggables.
339
                el.parents('[draggable="true"]')
340
                    .attr('data-inplace-in-draggable', true)
341
                    .attr('draggable', false);
342
 
343
                if (type === 'toggle') {
344
                    turnEditingOnToggle(el, options);
345
                } else if (type === 'select') {
346
                    turnEditingOnSelect(el, $.parseJSON(options));
347
                } else if (type === 'autocomplete') {
348
                    turnEditingOnAutocomplete(el, $.parseJSON(options));
349
                } else {
350
                    turnEditingOnText(el);
351
                }
352
            };
353
 
354
            // Turn editing on for the current element and register handler for Enter/Esc keys.
355
            turnEditingOffEverywhere();
356
            turnEditingOn(mainelement);
357
            editingEnabledPromise.resolve();
358
 
359
        });
360
 
361
 
362
        return {
363
            /**
364
             * Return an object to interact with the current inplace editables at a frontend level.
365
             *
366
             * @param {Element} parent the parent element containing a inplace editable
367
             * @returns {Object|undefined} an object to interact with the inplace element, or undefined
368
             *                             if no inplace editable is found.
369
             */
370
            getInplaceEditable: function(parent) {
371
                const element = parent.querySelector(`[data-inplaceeditable]`);
372
                if (!element) {
373
                    return undefined;
374
                }
375
                // Return an object to interact with the inplace editable.
376
                return {
377
                    element,
378
                    /**
379
                     * Get the value from the inplace editable.
380
                     *
381
                     * @returns {string} the current inplace value
382
                     */
383
                    getValue: function() {
384
                        return this.element.dataset.value;
385
                    },
386
                    /**
387
                     * Force a value change.
388
                     *
389
                     * @param {string} newvalue the new value
390
                     * @fires event:core/inplace_editable:updated
391
                     * @fires event:core/inplace_editable:updateFailed
392
                     */
393
                    setValue: function(newvalue) {
394
                        updateValue($(this.element), newvalue, true);
395
                    },
396
                    /**
397
                     * Return the inplace editable itemid.
398
                     *
399
                     * @returns {string} the current itemid
400
                     */
401
                    getItemId: function() {
402
                        return this.element.dataset.itemid;
403
                    },
404
                };
405
            }
406
        };
407
    });