##// END OF EJS Templates
codemirror: move some code outside of helpers for reuse
ergo -
r874:6903ccc9 default
parent child Browse files
Show More
@@ -1,530 +1,530 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Code Mirror
21 21 */
22 22 // global code-mirror logger;, to enable run
23 23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24 24
25 25 cmLog = Logger.get('CodeMirror');
26 26 cmLog.setLevel(Logger.OFF);
27 27
28 28
29 29 //global cache for inline forms
30 30 var userHintsCache = {};
31 31
32 // global timer, used to cancel async loading
33 var CodeMirrorLoadUserHintTimer;
34
35 var escapeRegExChars = function(value) {
36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 };
38
39 /**
40 * Load hints from external source returns an array of objects in a format
41 * that hinting lib requires
42 * @returns {Array}
43 */
44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 cmLog.debug('Loading mentions users via AJAX');
46 var _users = [];
47 $.ajax({
48 type: 'GET',
49 data: {query: query},
50 url: pyroutes.url('user_autocomplete_data'),
51 headers: {'X-PARTIAL-XHR': true},
52 async: true
53 })
54 .done(function(data) {
55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 $.each(data.suggestions, function(i) {
57 var userObj = data.suggestions[i];
58
59 if (userObj.username !== "default") {
60 _users.push({
61 text: userObj.username + " ",
62 org_text: userObj.username,
63 displayText: userObj.value_display, // search that field
64 // internal caches
65 _icon_link: userObj.icon_link,
66 _text: userObj.value_display,
67
68 render: function(elt, data, completion) {
69 var el = document.createElement('div');
70 el.className = "CodeMirror-hint-entry";
71 el.innerHTML = tmpl.format(
72 completion._icon_link, completion._text);
73 elt.appendChild(el);
74 }
75 });
76 }
77 });
78 cmLog.debug('Mention users loaded');
79 // set to global cache
80 userHintsCache[query] = _users;
81 triggerHints(userHintsCache[query]);
82 })
83 .fail(function(data, textStatus, xhr) {
84 alert("error processing request: " + textStatus);
85 });
86 };
87
88 /**
89 * filters the results based on the current context
90 * @param users
91 * @param context
92 * @returns {Array}
93 */
94 var CodeMirrorFilterUsers = function(users, context) {
95 var MAX_LIMIT = 10;
96 var filtered_users = [];
97 var curWord = context.string;
98
99 cmLog.debug('Filtering users based on query:', curWord);
100 $.each(users, function(i) {
101 var match = users[i];
102 var searchText = match.displayText;
103
104 if (!curWord ||
105 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
106 // reset state
107 match._text = match.displayText;
108 if (curWord) {
109 // do highlighting
110 var pattern = '(' + escapeRegExChars(curWord) + ')';
111 match._text = searchText.replace(
112 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
113 }
114
115 filtered_users.push(match);
116 }
117 // to not return to many results, use limit of filtered results
118 if (filtered_users.length > MAX_LIMIT) {
119 return false;
120 }
121 });
122
123 return filtered_users;
124 };
125
126 var CodeMirrorMentionHint = function(editor, callback, options) {
127 var cur = editor.getCursor();
128 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
129
130 // match on @ +1char
131 var tokenMatch = new RegExp(
132 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
133
134 var tokenStr = '';
135 if (tokenMatch !== null && tokenMatch.length > 0){
136 tokenStr = tokenMatch[0].strip();
137 } else {
138 // skip if we didn't match our token
139 return;
140 }
141
142 var context = {
143 start: (cur.ch - tokenStr.length) + 1,
144 end: cur.ch,
145 string: tokenStr.slice(1),
146 type: null
147 };
148
149 // case when we put the @sign in fron of a string,
150 // eg <@ we put it here>sometext then we need to prepend to text
151 if (context.end > cur.ch) {
152 context.start = context.start + 1; // we add to the @ sign
153 context.end = cur.ch; // don't eat front part just append
154 context.string = context.string.slice(1, cur.ch - context.start);
155 }
156
157 cmLog.debug('Mention context', context);
158
159 var triggerHints = function(userHints){
160 return callback({
161 list: CodeMirrorFilterUsers(userHints, context),
162 from: CodeMirror.Pos(cur.line, context.start),
163 to: CodeMirror.Pos(cur.line, context.end)
164 });
165 };
166
167 var queryBasedHintsCache = undefined;
168 // if we have something in the cache, try to fetch the query based cache
169 if (userHintsCache !== {}){
170 queryBasedHintsCache = userHintsCache[context.string];
171 }
172
173 if (queryBasedHintsCache !== undefined) {
174 cmLog.debug('Users loaded from cache');
175 triggerHints(queryBasedHintsCache);
176 } else {
177 // this takes care for async loading, and then displaying results
178 // and also propagates the userHintsCache
179 window.clearTimeout(CodeMirrorLoadUserHintTimer);
180 CodeMirrorLoadUserHintTimer = setTimeout(function() {
181 CodeMirrorLoadUserHints(context.string, triggerHints);
182 }, 300);
183 }
184 };
185
186 var CodeMirrorCompleteAfter = function(cm, pred) {
187 var options = {
188 completeSingle: false,
189 async: true,
190 closeOnUnfocus: true
191 };
192 var cur = cm.getCursor();
193 setTimeout(function() {
194 if (!cm.state.completionActive) {
195 cmLog.debug('Trigger mentions hinting');
196 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
197 }
198 }, 100);
199
200 // tell CodeMirror we didn't handle the key
201 // trick to trigger on a char but still complete it
202 return CodeMirror.Pass;
203 };
32 204
33 205 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
34 206 var ta = $('#' + textAreadId).get(0);
35 207 if (focus === undefined) {
36 208 focus = true;
37 209 }
38 210
39 211 // default options
40 212 var codeMirrorOptions = {
41 213 mode: "null",
42 214 lineNumbers: true,
43 215 indentUnit: 4,
44 216 autofocus: focus
45 217 };
46 218
47 219 if (options !== undefined) {
48 220 // extend with custom options
49 221 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
50 222 }
51 223
52 224 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
53 225
54 226 $('#reset').on('click', function(e) {
55 227 window.location = resetUrl;
56 228 });
57 229
58 230 return myCodeMirror;
59 231 };
60 232
61 233 var initCommentBoxCodeMirror = function(textAreaId, triggerActions){
62 234 var initialHeight = 100;
63 235
64 // global timer, used to cancel async loading
65 var loadUserHintTimer;
66
67 236 if (typeof userHintsCache === "undefined") {
68 237 userHintsCache = {};
69 238 cmLog.debug('Init empty cache for mentions');
70 239 }
71 240 if (!$(textAreaId).get(0)) {
72 241 cmLog.debug('Element for textarea not found', textAreaId);
73 242 return;
74 243 }
75 var escapeRegExChars = function(value) {
76 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
77 };
78 /**
79 * Load hints from external source returns an array of objects in a format
80 * that hinting lib requires
81 * @returns {Array}
82 */
83 var loadUserHints = function(query, triggerHints) {
84 cmLog.debug('Loading mentions users via AJAX');
85 var _users = [];
86 $.ajax({
87 type: 'GET',
88 data: {query: query},
89 url: pyroutes.url('user_autocomplete_data'),
90 headers: {'X-PARTIAL-XHR': true},
91 async: true
92 })
93 .done(function(data) {
94 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
95 $.each(data.suggestions, function(i) {
96 var userObj = data.suggestions[i];
97
98 if (userObj.username !== "default") {
99 _users.push({
100 text: userObj.username + " ",
101 org_text: userObj.username,
102 displayText: userObj.value_display, // search that field
103 // internal caches
104 _icon_link: userObj.icon_link,
105 _text: userObj.value_display,
106
107 render: function(elt, data, completion) {
108 var el = document.createElement('div');
109 el.className = "CodeMirror-hint-entry";
110 el.innerHTML = tmpl.format(
111 completion._icon_link, completion._text);
112 elt.appendChild(el);
113 }
114 });
115 }
116 });
117 cmLog.debug('Mention users loaded');
118 // set to global cache
119 userHintsCache[query] = _users;
120 triggerHints(userHintsCache[query]);
121 })
122 .fail(function(data, textStatus, xhr) {
123 alert("error processing request: " + textStatus);
124 });
125 };
126
127 /**
128 * filters the results based on the current context
129 * @param users
130 * @param context
131 * @returns {Array}
132 */
133 var filterUsers = function(users, context) {
134 var MAX_LIMIT = 10;
135 var filtered_users = [];
136 var curWord = context.string;
137
138 cmLog.debug('Filtering users based on query:', curWord);
139 $.each(users, function(i) {
140 var match = users[i];
141 var searchText = match.displayText;
142
143 if (!curWord ||
144 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
145 // reset state
146 match._text = match.displayText;
147 if (curWord) {
148 // do highlighting
149 var pattern = '(' + escapeRegExChars(curWord) + ')';
150 match._text = searchText.replace(
151 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
152 }
153
154 filtered_users.push(match);
155 }
156 // to not return to many results, use limit of filtered results
157 if (filtered_users.length > MAX_LIMIT) {
158 return false;
159 }
160 });
161
162 return filtered_users;
163 };
164
165 244 /**
166 245 * Filter action based on typed in text
167 246 * @param actions
168 247 * @param context
169 248 * @returns {Array}
170 249 */
171 250
172 251 var filterActions = function(actions, context){
173 252 var MAX_LIMIT = 10;
174 253 var filtered_actions= [];
175 254 var curWord = context.string;
176 255
177 256 cmLog.debug('Filtering actions based on query:', curWord);
178 257 $.each(actions, function(i) {
179 258 var match = actions[i];
180 259 var searchText = match.displayText;
181 260
182 261 if (!curWord ||
183 262 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
184 263 // reset state
185 264 match._text = match.displayText;
186 265 if (curWord) {
187 266 // do highlighting
188 267 var pattern = '(' + escapeRegExChars(curWord) + ')';
189 268 match._text = searchText.replace(
190 269 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
191 270 }
192 271
193 272 filtered_actions.push(match);
194 273 }
195 274 // to not return to many results, use limit of filtered results
196 275 if (filtered_actions.length > MAX_LIMIT) {
197 276 return false;
198 277 }
199 278 });
200 279 return filtered_actions;
201 280 };
202 281
203 var completeAfter = function(cm, pred) {
204 var options = {
205 completeSingle: false,
206 async: true,
207 closeOnUnfocus: true
208 };
209 var cur = cm.getCursor();
210 setTimeout(function() {
211 if (!cm.state.completionActive) {
212 cmLog.debug('Trigger mentions hinting');
213 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
214 }
215 }, 100);
216
217 // tell CodeMirror we didn't handle the key
218 // trick to trigger on a char but still complete it
219 return CodeMirror.Pass;
220 };
221
222 282 var submitForm = function(cm, pred) {
223 283 $(cm.display.input.textarea.form).submit();
224 284 return CodeMirror.Pass;
225 285 };
226 286
227 287 var completeActions = function(cm, pred) {
228 288 var cur = cm.getCursor();
229 289 var options = {
230 290 closeOnUnfocus: true
231 291 };
232 292 setTimeout(function() {
233 293 if (!cm.state.completionActive) {
234 294 cmLog.debug('Trigger actions hinting');
235 295 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
236 296 }
237 297 }, 100);
238 298 };
239 299
240 300 var extraKeys = {
241 "'@'": completeAfter,
301 "'@'": CodeMirrorCompleteAfter,
242 302 Tab: function(cm) {
243 303 // space indent instead of TABS
244 304 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
245 305 cm.replaceSelection(spaces);
246 306 }
247 307 };
248 308 // submit form on Meta-Enter
249 309 if (OSType === "mac") {
250 310 extraKeys["Cmd-Enter"] = submitForm;
251 311 }
252 312 else {
253 313 extraKeys["Ctrl-Enter"] = submitForm;
254 314 }
255 315
256 316 if (triggerActions) {
257 317 extraKeys["Ctrl-Space"] = completeActions;
258 318 }
259 319
260 320 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
261 321 lineNumbers: false,
262 322 indentUnit: 4,
263 323 viewportMargin: 30,
264 324 // this is a trick to trigger some logic behind codemirror placeholder
265 325 // it influences styling and behaviour.
266 326 placeholder: " ",
267 327 extraKeys: extraKeys,
268 328 lineWrapping: true
269 329 });
270 330
271 331 cm.setSize(null, initialHeight);
272 332 cm.setOption("mode", DEFAULT_RENDERER);
273 333 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
274 334 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
275 335 // start listening on changes to make auto-expanded editor
276 336 cm.on("change", function(self) {
277 337 var height = initialHeight;
278 338 var lines = self.lineCount();
279 339 if ( lines > 6 && lines < 20) {
280 340 height = "auto";
281 341 }
282 342 else if (lines >= 20){
283 343 zheight = 20*15;
284 344 }
285 345 self.setSize(null, height);
286 346 });
287 347
288 var mentionHint = function(editor, callback, options) {
289 var cur = editor.getCursor();
290 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
291
292 // match on @ +1char
293 var tokenMatch = new RegExp(
294 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
295
296 var tokenStr = '';
297 if (tokenMatch !== null && tokenMatch.length > 0){
298 tokenStr = tokenMatch[0].strip();
299 } else {
300 // skip if we didn't match our token
301 return;
302 }
303
304 var context = {
305 start: (cur.ch - tokenStr.length) + 1,
306 end: cur.ch,
307 string: tokenStr.slice(1),
308 type: null
309 };
310
311 // case when we put the @sign in fron of a string,
312 // eg <@ we put it here>sometext then we need to prepend to text
313 if (context.end > cur.ch) {
314 context.start = context.start + 1; // we add to the @ sign
315 context.end = cur.ch; // don't eat front part just append
316 context.string = context.string.slice(1, cur.ch - context.start);
317 }
318
319 cmLog.debug('Mention context', context);
320
321 var triggerHints = function(userHints){
322 return callback({
323 list: filterUsers(userHints, context),
324 from: CodeMirror.Pos(cur.line, context.start),
325 to: CodeMirror.Pos(cur.line, context.end)
326 });
327 };
328
329 var queryBasedHintsCache = undefined;
330 // if we have something in the cache, try to fetch the query based cache
331 if (userHintsCache !== {}){
332 queryBasedHintsCache = userHintsCache[context.string];
333 }
334
335 if (queryBasedHintsCache !== undefined) {
336 cmLog.debug('Users loaded from cache');
337 triggerHints(queryBasedHintsCache);
338 } else {
339 // this takes care for async loading, and then displaying results
340 // and also propagates the userHintsCache
341 window.clearTimeout(loadUserHintTimer);
342 loadUserHintTimer = setTimeout(function() {
343 loadUserHints(context.string, triggerHints);
344 }, 300);
345 }
346 };
347
348 348 var actionHint = function(editor, options) {
349 349 var cur = editor.getCursor();
350 350 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
351 351
352 352 var tokenMatch = new RegExp('[a-zA-Z]{1}[a-zA-Z]*$').exec(curLine);
353 353
354 354 var tokenStr = '';
355 355 if (tokenMatch !== null && tokenMatch.length > 0){
356 356 tokenStr = tokenMatch[0].strip();
357 357 }
358 358
359 359 var context = {
360 360 start: cur.ch - tokenStr.length,
361 361 end: cur.ch,
362 362 string: tokenStr,
363 363 type: null
364 364 };
365 365
366 366 var actions = [
367 367 {
368 368 text: "approve",
369 369 displayText: _gettext('Set status to Approved'),
370 370 hint: function(CodeMirror, data, completion) {
371 371 CodeMirror.replaceRange("", completion.from || data.from,
372 372 completion.to || data.to, "complete");
373 373 $('#change_status').select2("val", 'approved').trigger('change');
374 374 },
375 375 render: function(elt, data, completion) {
376 376 var el = document.createElement('div');
377 377 el.className = "flag_status flag_status_comment_box approved pull-left";
378 378 elt.appendChild(el);
379 379
380 380 el = document.createElement('span');
381 381 el.innerHTML = completion.displayText;
382 382 elt.appendChild(el);
383 383 }
384 384 },
385 385 {
386 386 text: "reject",
387 387 displayText: _gettext('Set status to Rejected'),
388 388 hint: function(CodeMirror, data, completion) {
389 389 CodeMirror.replaceRange("", completion.from || data.from,
390 390 completion.to || data.to, "complete");
391 391 $('#change_status').select2("val", 'rejected').trigger('change');
392 392 },
393 393 render: function(elt, data, completion) {
394 394 var el = document.createElement('div');
395 395 el.className = "flag_status flag_status_comment_box rejected pull-left";
396 396 elt.appendChild(el);
397 397
398 398 el = document.createElement('span');
399 399 el.innerHTML = completion.displayText;
400 400 elt.appendChild(el);
401 401 }
402 402 }
403 403 ];
404 404
405 405 return {
406 406 list: filterActions(actions, context),
407 407 from: CodeMirror.Pos(cur.line, context.start),
408 408 to: CodeMirror.Pos(cur.line, context.end)
409 409 };
410 410 };
411 CodeMirror.registerHelper("hint", "mentions", mentionHint);
411 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
412 412 CodeMirror.registerHelper("hint", "actions", actionHint);
413 413 return cm;
414 414 };
415 415
416 416 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
417 417 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
418 418 codeMirrorInstance.setOption("mode", mode);
419 419 };
420 420
421 421 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
422 422 codeMirrorInstance.setOption("lineWrapping", line_wrap);
423 423 };
424 424
425 425 var setCodeMirrorModeFromSelect = function(
426 426 targetSelect, targetFileInput, codeMirrorInstance, callback){
427 427
428 428 $(targetSelect).on('change', function(e) {
429 429 cmLog.debug('codemirror select2 mode change event !');
430 430 var selected = e.currentTarget;
431 431 var node = selected.options[selected.selectedIndex];
432 432 var mimetype = node.value;
433 433 cmLog.debug('picked mimetype', mimetype);
434 434 var new_mode = $(node).attr('mode');
435 435 setCodeMirrorMode(codeMirrorInstance, new_mode);
436 436 cmLog.debug('set new mode', new_mode);
437 437
438 438 //propose filename from picked mode
439 439 cmLog.debug('setting mimetype', mimetype);
440 440 var proposed_ext = getExtFromMimeType(mimetype);
441 441 cmLog.debug('file input', $(targetFileInput).val());
442 442 var file_data = getFilenameAndExt($(targetFileInput).val());
443 443 var filename = file_data.filename || 'filename1';
444 444 $(targetFileInput).val(filename + proposed_ext);
445 445 cmLog.debug('proposed file', filename + proposed_ext);
446 446
447 447
448 448 if (typeof(callback) === 'function') {
449 449 try {
450 450 cmLog.debug('running callback', callback);
451 451 callback(filename, mimetype, new_mode);
452 452 } catch (err) {
453 453 console.log('failed to run callback', callback, err);
454 454 }
455 455 }
456 456 cmLog.debug('finish iteration...');
457 457 });
458 458 };
459 459
460 460 var setCodeMirrorModeFromInput = function(
461 461 targetSelect, targetFileInput, codeMirrorInstance, callback) {
462 462
463 463 // on type the new filename set mode
464 464 $(targetFileInput).on('keyup', function(e) {
465 465 var file_data = getFilenameAndExt(this.value);
466 466 if (file_data.ext === null) {
467 467 return;
468 468 }
469 469
470 470 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
471 471 cmLog.debug('mimetype from file', file_data, mimetypes);
472 472 var detected_mode;
473 473 var detected_option;
474 474 for (var i in mimetypes) {
475 475 var mt = mimetypes[i];
476 476 if (!detected_mode) {
477 477 detected_mode = detectCodeMirrorMode(this.value, mt);
478 478 }
479 479
480 480 if (!detected_option) {
481 481 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
482 482 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
483 483 detected_option = mt;
484 484 }
485 485 }
486 486 }
487 487
488 488 cmLog.debug('detected mode', detected_mode);
489 489 cmLog.debug('detected option', detected_option);
490 490 if (detected_mode && detected_option){
491 491
492 492 $(targetSelect).select2("val", detected_option);
493 493 setCodeMirrorMode(codeMirrorInstance, detected_mode);
494 494
495 495 if(typeof(callback) === 'function'){
496 496 try{
497 497 cmLog.debug('running callback', callback);
498 498 var filename = file_data.filename + "." + file_data.ext;
499 499 callback(filename, detected_option, detected_mode);
500 500 }catch (err){
501 501 console.log('failed to run callback', callback, err);
502 502 }
503 503 }
504 504 }
505 505
506 506 });
507 507 };
508 508
509 509 var fillCodeMirrorOptions = function(targetSelect) {
510 510 //inject new modes, based on codeMirrors modeInfo object
511 511 var modes_select = $(targetSelect);
512 512 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
513 513 var m = CodeMirror.modeInfo[i];
514 514 var opt = new Option(m.name, m.mime);
515 515 $(opt).attr('mode', m.mode);
516 516 modes_select.append(opt);
517 517 }
518 518 };
519 519
520 520 var CodeMirrorPreviewEnable = function(edit_mode) {
521 521 // in case it a preview enabled mode enable the button
522 522 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
523 523 $('#render_preview').removeClass('hidden');
524 524 }
525 525 else {
526 526 if (!$('#render_preview').hasClass('hidden')) {
527 527 $('#render_preview').addClass('hidden');
528 528 }
529 529 }
530 530 };
General Comments 0
You need to be logged in to leave comments. Login now