Proyectos de Subversion Moodle

Rev

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