I'm writing a CKEditor widget. As I am trying to make it easily distributable, I want to make my css external. While this syntax works:
CKEDITOR.plugins.add('myplugin', {
requires: 'widget',
icons: 'myplugin',
init: function(editor) {
CKEDITOR.dialog.add('myplugin', this.path + 'dialogs/myplugin.js');
var self = this;
editor.on('contentDom', function() {
editor.document.appendStyleSheet(CKEDITOR.getUrl(self.path + 'css/style.css'));
});
}
});
I found that CKSource uses onLoad: function(editor){} in their image2 widget to attach css. This looks more canonical than my code. I'm getting editor = undefined though. What's the catch?
Update:
I'm using the framed option and I definitely want to stay DRY through .appendStyleSheet() to user-facing page as well as to the wysiwyg framed editor. However, I still have problem with init/beforeInit:function(editor), where editor does not seem to have document property populated at that very moment. When I do window.ed = editor though, then I can access the window.ed.document in the console once the page is ready. So, is there any kind of deferred promise going on there? Maybe I'm complicating things, but I definitely want to append a distributable stylesheet and remove it easily.
I found this working:
beforeInit: function(
var ccss = CKEDITOR.config.contentsCss;
if(typeof ccss == 'string'){
ccss = [ccss];
}
ccss.push('/css/style.css'); // <-- my css installed at website root
CKEDITOR.config.contentsCss = ccss;
console.log(CKEDITOR.config.contentsCss);
},
While the issue with isolated css stylesheet might sound over-complicated, I want to achieve an easily demoable state without the need to touch existing files.
Note that in the image2 plugin stylesheet is not added to editor instance, but using CKEDITOR.addCss:
CKEDITOR.plugins.add( 'image2', {
...
onLoad: function( editor ) {
CKEDITOR.addCss( ' ... ' );
}
} );
It is a mistake that the editor is specified as an argument - it always will be null.
If you want to add stylesheet manually using document.appendStyleSheet, then do it on init, or beforeInit, not on onLoad.
Altough, you should know that contentDom will be fired multiple times for one document, e.g. when setting data. So in your case stylesheet would be added many times. Therefore, if you want to add a stylesheet for the framed editor (the one using iframe), you can use config.contentsCss, and if you want to add it for inline editor, then you can freely call:
CKEDITOR.document.appendStyleSheet( ... );
In plugin's onLoad (will be called only once). So most likely you'll want to do both things, to handle inline and framed editors.
Related
I'm working to use custom checkbox styles with a checkbox which is dynamically generated by javascript for the Google Identity Toolkit. For example, we add this div:
<div id="gitkitWidgetDiv"></div>
And the Google Identity Toolkit script generates new html for that div.
I need to add a class to the HTML which is added by the javascript without any action by the user and I'm struggling to make it work. For example, here is my code:
$("#gitkitWidgetDiv").on('ready', ".gitkit-sign-in-options label", function() {
$(this).addClass('checkbox');
});
I've tried switching 'ready' for a few other options and also using the livequery plugin, but nothing is working for me. It works if I use an active event like 'click,' but I can't figure out how to do this when the page loads. Could someone please help? Thanks!
Modern browsers (including IE11) support mutation obervers. You can use one to monitor the parent node of the div that will be added. When the div has been added, just add the class.
Here's something I made which comes in handy in annoying cases like this where it's difficult to tell when the element you need has finished loading in: https://gist.github.com/DanWebb/8b688b31492632b38aea
so after including the function it'd be something like:
var interval = 500,
stopTime = 5000,
loaded = false;
setIntervalTimeout(function() {
if($('.dynanicElementClass').length && !loaded) {
$('.dynanicElementClass').addClass('checkbox');
loaded = true;
}
}, interval, stopTime);
It's not perfect and I'm sure there are better solutions out there but in most cases like this it does the job.
I am using TinyMCE and I want to set the default font-size to 12.
Here, I assigned a variable to the active TinyMCE editor, but it's not working...
HTML content:
<textarea id="text_message"></textarea>
JavaScript:
$("#text_message").tinymce({
// Location of TinyMCE script
script_url: '/static/app/a/scripts/tinymce/jscripts/tiny_mce/tiny_mce.js'
});
var fontSize = 12;
tinyMCE.activeEditor.formatter.apply('fontSize',
{
value: fontSize
});
There's a couple of different event stages you should wait for:
The page is ready in some state. (tinyMCE.init() can also be used, depending on the situation.)
The editor has been initialized and is ready for you to being working on it.
The first can be accomplished using either:
$(document).ready()
Or:
$(window).load()
Both handle events related to the readiness of the page. $.ready() triggers when the DOM is available, $.load() in the case of window triggers when the page content has been loaded (such as inline img, script, and link sources). .ready() fires slightly before the other, but in many cases either will work.
Note, below I've got jQuery(function($){...}); which is a shortcut to $(document).ready(); and helps manage conflicts with other frameworks that use $ globally.
At the second stage you need to add an initialization event handler to your editor creation request, such as:
$.tinymce({ init_instance_callback: function(){ ... } });
In full:
$tiny.tinymce({
// ... other stuff here, comma-delineated
init_instance_callback: function(){
ac = tinyMCE.activeEditor;
ac.dom.setStyle(ac.getBody(), 'fontSize', fontSize);
}
// ... other stuff here, comma-delineated
});
Here is where you can set the display value for font-size (using the Javascript property of fontSize, since Javascript doesn't allow - in labels such as font-size). This waits for the editor you're adding to initialize and say "Hey, I'm here now". Once you get that notification of the vent firing, you can set the editor body (what ac.getBody() returns) to be font-size: 18px.
What this means is that the font-size will not be returned once you choose the save the editor content; it's above it in in the editor "body" (literally, the iframe's body element), in most cases (unless you choose the plugins : "fullpage" initialization option). So it's a "default", but does not get set explicitly within the content itself.
If you have Chrome or Firefox with Firebug add-on installed, watch the console on this fiddle:
http://jsfiddle.net/userdude/9PuUx/1/
Here is the working code demo and link to a fiddle demo (I use 18px to easily see the change):
jQuery(function a($){
var $tiny = $("#text_message"),
fontSize = '18px',
ac;
$tiny.tinymce({
mode : "textareas",
theme : "advanced",
theme_advanced_buttons1 : "fontsizeselect,bold,underline,italic",
init_instance_callback: function(){
ac = tinyMCE.activeEditor;
ac.dom.setStyle(ac.getBody(), 'fontSize', fontSize);
}
});
});
http://jsfiddle.net/userdude/9PuUx/
The tinyMCE documentation and API can be a bit confusing and lacking, and I'm not an expert in using it either, so I know there's perhaps four or five other ways to interpret what's going on and how to do this. Your mileage may vary. If you have an issue with the way I did this or it does not seem to do what you want or expect, just leave a comment and let me know.
I've got a lot of custom buttons on my TinyMCE toolbar, most of which open a dialog box with some further options in when you click them. This all works fine.
Here is an example of something in my tinyMCE_setup() function:
ed.addButton('link2', {
title: '{!link!}',
image: '../style/common/images/link_20x20.png',
onclick: function() {
replyBoxDialog('link', ed);
}
});
However, I want to be able to call these programatically, and faking a .click() on the button with jQuery won't cut it.
I've tried calling the function directly
replyBoxDialog('link',tinyMCE);
But no matter what I try as the second argument, I can't get the right object (so it fails when it's time to insert something into the editor, as it doesn't know what the editor is).
I've also had a try with various execCommand() calls, but I've no idea what to put in there.
Any clues?
All you have to do is to use a real editor object as paramter
var editor_instance = tinymce.activeEditor; // in case you just use one editor
var editor_instance = tinymce.get('my_special_editor_id'); // in case you have more than one editor
replyBoxDialog('link', editor_instance);
I've managed to make it work by creating a variable 'globalEd' at the top of the script and adding globalEd = ed; to tinyMCE_setup(), then I can call replyBoxDialog('dragndrop', globalEd);. This seems like a properly hacky way of doing things though, so I'd welcome any further advice.
I am trying to load Skyscanner API dynamically but it doesn't seem to work. I tried every possible way I could think of and all it happens the content disappears.
I tried console.log which gives no results; I tried elements from chrome's developers tools and while all the content's css remains the same, still the content disappears (I thought it could be adding display:none on the html/body sort of). I tried all Google's asynch tricks, yet again blank page. I tried all js plugins for async loading with still the same results.
Skyscanner's API documentation is poor and while they offer a callback it doesn't work the way google's API's callback do.
Example: http://jsfiddle.net/7TWYC/
Example with loading API in head section: http://jsfiddle.net/s2HkR/
So how can I load the api on button click or async? Without the file being in the HEAD section. If there is a way to prevent the document.write to make the page blank or any other way. I wouldn't mind using plain js, jQuery or PHP.
EDIT:
I've set a bounty to 250 ontop of the 50 I had previously.
Orlando Leite answered a really close idea on how to make this asynch api load although some features doesn't work such as selecting dates and I am not able to set styling.
I am looking for an answer of which I will be able to use all the features so that it works as it would work if it was loading on load.
Here is the updated fiddle by Orlando: http://jsfiddle.net/cxysA/12/
-
EDIT 2 ON Gijs ANSWER:
Gijs mentioned two links onto overwriting document.write. That sounds an awesome idea but I think it is not possible to accomplish what I am trying.
I used John's Resig way to prevent document.write of which can be found here: http://ejohn.org/blog/xhtml-documentwrite-and-adsense/
When I used this method, I load the API successfuly but the snippets.js file is not loading at all.
Fiddle: http://jsfiddle.net/9HX7N/
I belive what you want is it:
function loadSkyscanner()
{
function loaded()
{
t.skyscanner.load('snippets', '1', {'nocss' : true});
var snippet = new t.skyscanner.snippets.SearchPanelControl();
snippet.setCurrency('GBP');
snippet.setDeparture('uk');
snippet.draw(document.getElementById('snippet_searchpanel'));
}
var t = document.getElementById('sky_loader').contentWindow;
var head = t.document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.onreadystatechange= function() {
if(this.readyState == 'complete') loaded();
}
script.onload= loaded;
script.src= 'http://api.skyscanner.net/api.ashx?key=PUT_HERE_YOUR_SKYSCANNER_API_KEY';
head.appendChild(script);
}
$("button").click(function(e)
{
loadSkyscanner();
});
It's load skyscanner in iframe#sky_loader, after call loaded function to create the SearchPanelControl. But in the end, snippet draws in the main document. It's really a bizarre workaround, but it works.
The only restriction is, you need a iframe. But you can hide it using display:none.
A working example
EDIT
Sorry guy, I didn't see it. Now we can see how awful is skyscanner API. It puts two divs to make the autocomplete, but not relative to the element you call to draw, but the document.
When a script is loaded in a iframe, document is the iframe document.
There is a solution, but I don't recommend, is really a workaround:
function loadSkyscanner()
{
var t;
this.skyscanner;
var iframe = $("<iframe id=\"sky_loader\" src=\"http://fiddle.jshell.net/orlleite/2TqDu/6/show/\"></iframe>");
function realWorkaround()
{
var tbody = t.document.getElementsByTagName("body")[0];
var body = document.getElementsByTagName("body")[0];
while( tbody.children.length != 0 )
{
var temp = tbody.children[0];
tbody.removeChild( temp );
body.appendChild( temp );
}
}
function snippetLoaded()
{
skyscanner = t.skyscanner;
var snippet = new skyscanner.snippets.SearchPanelControl();
snippet.setCurrency('GBP');
snippet.setDeparture('uk');
snippet.draw(document.getElementById('snippet_searchpanel'));
setTimeout( realWorkaround, 2000 );
}
var loaded = function()
{
console.log( "loaded" );
t = document.getElementById('sky_loader').contentWindow;
t.onLoadSnippets( snippetLoaded );
}
$("body").append(iframe);
iframe.load(loaded);
}
$("button").click(function(e)
{
loadSkyscanner();
});
Load a iframe with another html who loads and callback when the snippet is loaded. After loaded create the snippet where you want and after set a timeout because we can't know when the SearchPanelControl is loaded. This realWorkaround move the autocomplete divs to the main document.
You can see a work example here
The iframe loaded is this
EDIT
Fixed the bug you found and updated the link.
the for loop has gone and added a while, works better now.
while( tbody.children.length != 0 )
{
var temp = tbody.children[0];
tbody.removeChild( temp );
body.appendChild( temp );
}
For problematic cases like this, you can just overwrite document.write. Hacky as hell, but it works and you get to decide where all the content goes. See eg. this blogpost by John Resig. This ignores IE, but with a bit of work the trick works in IE as well, see eg. this blogpost.
So, I'd suggest overwriting document.write with your own function, batch up the output where necessary, and put it where you like (eg. in a div at the bottom of your <body>'). That should prevent the script from nuking your page's content.
Edit: OK, so I had/took some time to look into this script. For future reference, use something like http://jsbeautifier.org/ to investigate third-party scripts. Much easier to read that way. Fortunately, there is barely any obfuscation/minification at all, and so you have a supplement for their API documentation (which I was unable to find, by the way -- I only found 'code wizards', which I had no interest in).
Here's an almost-working example: http://jsfiddle.net/a8q2s/1/
Here's the steps I took:
override document.write. This needs to happen before you load the initial script. Your replacement function should append their string of code into the DOM. Don't call the old document.write, that'll just get you errors and won't do what you want anyway. In this case you're lucky because all the content is in a single document.write call (check the source of the initial script). If this weren't the case, you'd have to batch everything up until the HTML they'd given you was valid and/or you were sure there was nothing else coming.
load the initial script on the button click with jQuery's $.getScript or equivalent. Pass a callback function (I used a named function reference for clarity, but you can inline it if you prefer).
Tell Skyscanner to load the module.
Edit #2: Hah, they have an API (skyscanner.loadAndWait) for getting a callback once their script has loaded. Using that works:
http://jsfiddle.net/a8q2s/3/
(note: this still seems to use a timeout loop internally)
In the skyrunner.js file they are using document.write to make the page blank on load call back... So here are some consequences in your scenario..
This is making page blank when you click on button.
So, it removes everything from page even 'jQuery.js' that is why call back is not working.. i.e main function is cannot be invoked as this is written using jQuery.
And you have missed a target 'div' tag with id = map(according to the code). Actually this is the target where map loads.
Another thing i have observed is maps is not actually a div in current context, that is maps api to load.
Here you must go with the Old school approach, That is.. You should include your skyrunner.js file at the top of the head content.
So try downloading that file and include in head tag.
Thanks
A few of my views need their textareas converted to rich text editors.
I'm using jwysiwyg as the editor. It requires that the element it is being attached to is in the page when the editor is initialized i.e. when I call $(this.el).wysiwyg(), this.el is already in the document.
Most of my views do not actually attach themselves to the dom - their render methods simply set their elements html content using the apps templating engine e.g. $(this.el).html(this.template(content)
Views/Controllers further up the chain look after actually inserting these child views into the page. At the same time, views do re-render themselves when their models change.
How do I ensure that the editor is attached to the element every time its rendered and still ensure that the editor is not attached until the element is already in the page?
Obviously I could hack something together that would work in this particular case but I would like an elegant solution that will work for all cases.
Any help would be much appreciated.
Edit: The main point here is that the solution must scale gracefully to cover multiple elements that must be styled after rendering and must not be styled until they are in the DOM
Edit: This is not an issue if I do top-down rendering but this is slow, I'd like a solution whereby I can render from the bottom up and then insert the complete view in one go at the top
Edit:
Using a combination of some of the techniques suggested below I'm thinking of doing something like the following. Any comments/critique would be appreciated.
app/views/base_view.js:
initialize: function() {
// wrap the render function to trigger 'render' events
this.render = _.wrap(this.render, function() {
this.trigger('render')
});
// bind this view to 'attach' events.
// 'attach' events must be triggered manually on any view being inserted into the dom
this.bind('attach', function() {
this.attached();
// the refreshed event is only attached to the render event after the view has been attached
this.bind('render', this.refreshed())
// each view must keep a record of its child views and propagate the 'attach' events
_.each(this.childViews, function(view) {
view.trigger('attach')
})
})
}
// called when the view is first inserted to the dom
attached: function() {
this.style();
}
// called if the view renders after it has been inserted
refreshed: function() {
this.style();
}
style: function() {
// default styling here, override or extend for custom
}
What if you used the JQuery LiveQuery Plugin to attach the editor? Such code could be a part of your template code, but not as HTML, but as Javascript associated with the template. Or you could add this globally. The code might look like this (assuming you've included the plugin itself):
$('textarea.wysiwyg').livequery(function() {
$(this).wysiwyg();
});
I have not tested this code, but in theory it should match an instance of a textarea element with a class of 'wysiwyg' when it appears in the DOM and call the wysiwyg function to apply the editor.
To adhere to DRY principle and get an elegant solution, you'll want a function dedicated to determining if a textarea has wysiwyg, let's say wysiwygAdder and add it if necessary. Then you can use underscore's wrap function to append your wysiwyg adder to the end of the render function.
var View = Backbone.View.extend({
el: '#somewhere',
initialize: function(){
_.bind(this, "render");
this.render = _.wrap(this.render, wysiwygAdder);
},
render: function(){
//Do your regular templating
return this;//allows wysiwygAdder to pick up context
}
});
function wysiwygAdder(context){
$('textarea', context).doYourStuff();
//check for presence of WYSIWYG, add here
}
When the view is initialized, it overwrites your render function with your render function, followed by wysiwygAdder. Make sure to return this; from render to provide context.
One solution would be to use event delegation and bind the focus event to check whether the rich text editor had been loaded or not. That way the user would get the text editor when they needed it (via the lazy loading, a minor performance improvement) and you wouldn't have to load it otherwise. It would also eliminate needing to worry about when to attach the rich text editor and that being dependent on the rendering chain.
If you're worried about the FOUC (flash of unstyled content) you could simply style the un-modified text areas to contain an element with a background image the looked just like the wysiwyg controls and have your focus binding toggle a class to hide the facade once the rich text editor had taken over.
Here's a sample of what I had in mind:
var View = Backbone.View.extend({
el: '#thing',
template: _.template($("#template").html()),
render: function() {
// render me
$(this.el).html(this.template(context));
// setup my textareas to launch a rich text area and hide the facade
$(this.el).delegate('focus', 'textarea', function() {
if(!$(this).hasRichTextEditor()) { // pseudocode
$(this).wysiwyg();
$(this).find('.facade').toggle();
}
});
}
});
Great problem to solve! Not too sure I've got the entire jist but... You may be able to get away with a 'construction_yard' (I just made that term up) that's way off to the left, build and place items there, then just move them when they're ready to be placed. Something along the lines of:
.construction_yard {
position: absolute;
left: -10000000000px;
}
This solution may fix several problems that might crop up. For example jquery height and width attributes on something that's 'hidden' are 0, so if you are styling along those lines, you'd have to wait till it was placed, which is more complicated, and jumbles things up.
your views would then need to do something along the lines of (pseudo-code):
//parent
//do all your view-y stuff...
foreach (child = this.my_kids) {
if child.is_ready() {
put_that_child_in_its_place();
}
}
Similarly, for children, you'd do a similar thing:
//child
foreach(parent = this.my_parents) {
if parent.isnt_ready() {
this.go_play_in_construction_yard();
} else {
this.go_to_parents_house();
}
}
... and, since backbone is pretty easy to extend, you could wrap it up in a more generalized class using:
var ParentsAndChildrenView = Backbone.View.extend({blah: blah});
and then
var Wsywig = ParentsAndChildrenView.extend({blah: blah});
hope that helps!
Almost forgot to note my source:
http://jqueryui.com/demos/tabs/#...my_slider.2C_Google_Map.2C_sIFR_etc._not_work_when_placed_in_a_hidden_.28inactive.29_tab.3F