sferik/rails_admin — pull request #1217 by rovermicrover (2012-06-21 04:49:12+00:00)
Title: Javascript fix for codemirror and ckeditor. Forces a wait until the scri...

Sorry I did it again, I forgot to squash all the commits down. >_<

Forward:
On a few project I have been working on that incorporate rails_admin I noticed that sometimes when going to edit or create a new object Ckeditor/CodeMirror would fail to load. A simple refresh would fix the issue most times, but it kind of bothered me.

Then on one project the client narrowed down and focused on the bug, and got very unhappy about it, and let me know that they shouldn't have to refresh. So I set off on my merry way today to try and address the issue.

Overview:
Currently Ckeditor and CodeMirror are evoked in nearly the same fashion. (When I submitted my CodeMirror patch I just tried to keep it in line with what was already there.)

```coffee
$('form [data-richtext=ckeditor]').not('.ckeditored').each ->
options = $(this).data('options')
window.CKEDITOR_BASEPATH = options['base_location']
if not window.CKEDITOR
$(window.document).append('<\/script>')
if instance = window.CKEDITOR.instances[this.id]
instance.destroy(true)
window.CKEDITOR.replace(this, options['options'])
$(this).addClass('ckeditored')
```

The issue I was running into I found out was that the .each was executing concurrently sometimes even trying to load multiple instances of Ckeditor.js/Codemirror.js if more then one of a type was declared, and that the script was sometimes moving on before Ckeditor/CodeMirror had finished loading, leading to a no method instances for undefined. In one bizarre case I ran into it also kept overwriting window.CKEDITOR_BASEPATH, though I was never really able to track down why that was happening.

Solution:
In order to address this I changed them both to something like this. (I am using the new Ckeditor code as the example.)

```coffee
# ckeditor

goCkeditors = (array) =>
array.each (index, domEle) ->
options = $(this).data
if instance = window.CKEDITOR.instances[this.id]
instance.destroy(true)
window.CKEDITOR.replace(this, options['options'])
$(this).addClass('ckeditored')

array = $('form [data-richtext=ckeditor]').not('.ckeditored')
if array.length
@array = array
if not window.CKEDITOR
options = $(array[0]).data('options')
window.CKEDITOR_BASEPATH = options['base_location']
$.getScript options['jspath'], (script, textStatus, jqXHR) =>
goCkeditors(@array)
else
goCkeditors(@array)
```

The first thing I do is get an array with all the objects that need to have ckeditor called on them.
```coffee
array = $('form [data-richtext=ckeditor]').not('.ckeditored')
```

I then test to see if there are any elements returned by the above.
```coffee
if array.length
```

If ckeditor is not present at all then I load options from the first entry in the array, set the base path and load the script
```coffee
if not window.CKEDITOR
options = $(array[0]).data('options')
window.CKEDITOR_BASEPATH = options['base_location']
$.getScript options['jspath'], (script, textStatus, jqXHR) =>
goCkeditors(@array)
```

I then call the following when the script is done loading
```coffee
goCkeditors(@array)
```

Which then goes through each element that needs to be have ckeditor called on it, while loading each ones unquie settings. (This is basicly the old code)
```coffee
goCkeditors = (array) =>
array.each (index, domEle) ->
options = $(this).data
if instance = window.CKEDITOR.instances[this.id]
instance.destroy(true)
window.CKEDITOR.replace(this, options['options'])
$(this).addClass('ckeditored')
```

On the other hand if ckeditor has already been called then I just call the following and call it a day
```coffee
else
goCkeditors(@array)
```

The logic for the CodeMirror changes are basically the same.

Upsides Of These Commits:
Ensures that Ckeditor and CodeMirror will always load correctly by delaying any thing relating to themselves until their corresponding base scripts have been downloaded.

Stop the chance of having a double or more load of Ckeditor and/or CodeMirror and slowing down your backend or your server/CDN.

Downside Of These Commits:
The Ckeditor/CodeMirror boxes stay textarea for a few more miliseconds before changing into their real forms. This might best be dealt with by setting the whole array to hidden and only showing them when they are about to have Ckeditor/CodeMirror called on them, then again this will cause the load to take a little more time.

SideNote:
I also added in a change to ckeditor_config_js and ckeditor_location. If either of these are nil they will now be determined by ckeditor_base_location + their respective file names. There defaults are now also nil.

I hope this is helpful and let me know if you have any questions.

Also here are the test results for thoroughness sake.

```
Run options: exclude {:mongoid=>true, :skip_active_record=>true}
................................................................................................
Finished in 1 minute 34.22 seconds
442 examples, 0 failures

Randomized with seed 740
```