Multiple instances of CodeMirror textarea on the same page - javascript

I'm trying to make a little project for job interview questions and have a bunch of questions with answers and code examples on the page. Each is inside a .collapsible div from Materialize.css, and when clicked shows the answer and code example.
What is the best way to go about this? I tried putting initializer into a function, grabbing all my textareas form the DOM, looping through them and turning them into CodeMirror textareas.
$(document).ready(function(){
var blocks = document.getElementsByClassName('code-block');
function createEditorFrom(selector) {
let editor = CodeMirror.fromTextArea(selector, {
lineNumbers : false,
mode: "swift",
});
}
for (var x = 0; x < blocks.length; x++) {
createEditorFrom(blocks[x]);
}
// Callback for Collapsible open
$('.collapsible').collapsible({
onOpen: function() {
// call editor.refresh()?
},
});
});
This does work but I feel like it is not a very elegant way of solving this issue. Is there a better way to do this?
Questions:
Is there a better way to create all the CodeMirror textareas?
The code in the editors does not appear until clicked. Nothing I do makes it work. Calling editor.refresh() (with setTimeout), and autorefresh: true.

One thing you will have to do anyhow is keeping a reference to your CodeMirror instances (eg. to get/set their value), so createEditorForm will have to return the CodeMirror instance, & you could for example push them to an array in the loop:
function createEditorFrom(selector) {
return CodeMirror.fromTextArea(selector, {
lineNumbers : false,
mode: "swift",
});
}
let codeblocks = [];
for (var x = 0; x < blocks.length; x++) {
codeblocks.push({
CM: createEditorFrom(blocks[x]),
el: blocks[x]
});
}
I've encountered a similar problem to "The code in the editors does not appear until clicked" and I even wrote a comment next to the solution. Translated to your implementation that would be:
function createEditorFrom(selector) {
var instance = CodeMirror.fromTextArea(selector, {
lineNumbers : false,
mode: "swift",
});
// make sure the CodeMirror unit expands by setting a null character
instance.setValue('\0');
instance.setValue('');
return instance;
}
For what it's worth, I also used some CSS tweaks to ensure consistent initial & max-height rendering:
/* CodeMirror */
.CodeMirror { height: auto; overflow: auto; max-height: 250px; }
.CodeMirror-scroll { height: auto; min-height: 24px; }
Is there a better way to create all the CodeMirror textareas [than by looping through them]?
Yes, you can lazy initialize them if they are not visible on page load (and actually I believe it's better to initialize them only when the parent node of the <textarea> is visible, I'm not sure if CodeMirror requires render-info (like Element.getBoundingClientRect), e.g. only load them onOpen of their collapsible parent.

I ended up initializing each one when the collapsible is opened. This works but I guess it would need to be adjusted if there were multiple CodeMirror textareas in the same collapsible.
I also added in a check becuase otherwise when the collapsible gets opened, then closed, then reopened it would create a duplicate textarea.
$(document).ready(function(){
$('.collapsible').collapsible({
onOpen: createCodeBlock
});
function createCodeBlock(target) {
var card = target.context.parentElement.parentElement;
var textarea = card.getElementsByClassName('code-block')[0];
// Placeholder class that prevents the duplicate creation of
// the same textarea when the collapsible is closed then reopened.
if (!textarea.classList.contains("created")) {
CodeMirror.fromTextArea(textarea, {
lineNumbers: false,
mode: "swift",
});
textarea.className += "created";
}
}
});

Related

Best way to hide 10000 dropdown menus

Context -
I have a chat component and each individual chat message has a dropdown.
And the dropdown menu is opened by clicking the "More Options icon"(3 dots).
Each individual chat message is a "backbone item view"
One solution is to listen to click on "body", loop through all the menus and then close the dropdown by removing a class on it.
$("body").on("click", function() {
$(".drop-down-menu").each(function(idx, item) {
$(item).removeClass("open"); // open class indicated it is open via CSS
});
});
The CSS -
.drop-down-menu {
visibility: hidden;
opacity: 0;
&.open {
opacity: 1;
visibility: visible;
}
}
Will there be any performance impact if there are 10,000 messages or more?
Hence, I am looking for the best solution to hide the drop down if user clicks anywhere on the screen.
Thanks.
You can make some trivial changes that should improve the performance of your code. The first thing is that there's no reason to loop like you are doing. jQuery objects are collections and jQuery operations usually loop over the elements of a jQuery object. So:
$("body").on("click", function() {
$(".drop-down-menu").removeClass("open");
});
This will automatically remove the class open from all elements matched by the selector ".drop-down-menu". jQuery will still go over a loop internally, but it is faster to let jQuery iterate by itself than to have .each call your own callback and then inside the callback create a new jQuery object on which to call .removeClass.
Furthermore, you logically know that removing the open class from elements that do not have this class is pointless. So you can narrow the operation to only those elements where removing open makes sense:
$("body").on("click", function() {
$(".drop-down-menu.open").removeClass("open");
});
These are principles that are widely applicable and that have trivial cost to implement. Anything more than this runs into the realm of optimizations that may have downsides, and should be supported by actually profiling your code. You could replace the jQuery code with code that only uses stock DOM calls but then if you need support for old browsers the cost of dealing with this and that quirk may not be worth it. And if you are using stock DOM methods, there are different approaches that may yield different performance increases, at the cost of code complexity.
Louis is offering a quick fix with efficient jQuery selectors.
For the long run, I would suggest making each message a MessageView component which has a ContextMenuView component. That way, each view only has one menu to take care of.
Catching clicks outside of an element
Then, use the following ClickOutside view as the context menu base view. It looks complicated, but it only wraps the blur and focus DOM events to know if you clicked outside the view.
It offers a simple onClickOutside callback for the view itself and a click:outside event which is triggered on the element.
The menu view now only has to implement the following:
var ContextMenuView = ClickOutside.extend({
toggle: function(val) {
this.$el.toggleClass("open", val);
this.focus(); // little requirement
},
// here's where the magic happens!
onClickOutside: function() {
this.$el.removeClass("open");
}
});
See the demo
var app = {};
(function() {
var $body = Backbone.$(document.body);
/**
* Backbone view mixin that enables the view to catch simulated
* "click:outside" events (or simple callback) by tracking the
* mouse and focusing the element.
*
* Additional information: Since the blur event is triggered on a mouse
* button pressed and the click is triggered on mouse button released, the
* blur callback gets called first which then listen for click event on the
* body to trigger the simulated outside click.
*/
var ClickOutside = app.ClickOutside = Backbone.View.extend({
events: {
"mouseleave": "_onMouseLeave",
"mouseenter": "_onMouseEnter",
"blur": "_onBlur",
},
/**
* Overwrite the default constructor to extends events.
*/
constructor: function() {
this.mouseInside = false;
var proto = ClickOutside.prototype;
this.events = _.extend({}, proto.events, this.events);
ClickOutside.__super__.constructor.apply(this, arguments);
this.clickOnceEventName = 'click.once' + this.cid;
},
/**
* Hijack this private method to ensure the element has
* the tabindex attribute and is ready to be used.
*/
_setElement: function(el) {
ClickOutside.__super__._setElement.apply(this, arguments);
var focusEl = this.focusEl;
if (focusEl && !this.$focusElem) {
this.$focusElem = focusEl;
if (!(focusEl instanceof Backbone.$)) {
this.$focusElem = Backbone.$(focusEl);
}
} else {
this.$focusElem = this.$el;
}
this.$focusElem.attr('tabindex', -1);
},
focus: function() {
this.$focusElem.focus();
},
unfocus: function() {
this.$focusElem.blur();
$body.off(this.clickOnceEventName);
},
isMouseInside: function() {
return this.mouseInside;
},
////////////////////////////
// private Event handlers //
////////////////////////////
onClickOutside: _.noop,
_onClickOutside: function(e) {
this.onClickOutside(e);
this.$focusElem.trigger("click:outside", e);
},
_onBlur: function(e) {
var $focusElem = this.$focusElem;
if (!this.isMouseInside() && $focusElem.is(':visible')) {
$body.one(this.clickOnceEventName, this._onClickOutside.bind(this));
} else {
$focusElem.focus(); // refocus on inside click
}
},
_onMouseEnter: function(e) {
this.mouseInside = true;
},
_onMouseLeave: function(e) {
this.mouseInside = false;
},
});
var DropdownView = app.Dropdown = ClickOutside.extend({
toggle: function(val) {
this.$el.toggle(val);
this.focus();
},
onClickOutside: function() {
this.$el.hide();
}
});
})();
var DemoView = Backbone.View.extend({
className: "demo-view",
template: $("#demo-template").html(),
events: {
"click .toggle": "onToggleClick",
},
initialize: function() {
this.dropdown = new app.Dropdown();
},
render: function() {
this.$el.html(this.template);
this.dropdown.setElement(this.$(".dropdown"));
return this;
},
onToggleClick: function() {
this.dropdown.toggle(true);
},
});
$("#app")
.append(new DemoView().render().el)
.append(new DemoView().render().el);
html,
body {
height: 100%;
width: 100%;
}
.demo-view {
position: relative;
margin-bottom: 10px;
}
.dropdown {
z-index: 2;
position: absolute;
top: 100%;
background-color: gray;
padding: 10px;
outline: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<div id="app"></div>
<script type="text/template" id="demo-template">
<button type="button" class="toggle">Toggle</button>
<div class="dropdown" style="display:none;">
This is a drop down menu.
</div>
</script>
Alternatives to detect a click outside an element
If you don't want, or can't use blur and focus events, take a look at How do I detect a click outside an element? for alternative techniques.
Lazy initialization of views
Another way to make an SPA more efficient is to delay the creation of new view to the very moment you need it. Instead a creating 10k context menu views, wait for the first time the user clicks on the toggle button and create a new view if it doesn't exist yet.
toggleMenu: function(){
var menuView = this.menuView;
if (!menuView) {
menuView = this.menuView = new ContextMenuView();
this.$('.dropdown').html(menuView.render().el);
}
menuView.toggle();
}
Pagination
Passed a certain threshold of HTML inside a webpage, the browser starts to lag and it impedes the user experience. Instead of dumping 10k views into a div, only show like a 100, or the minimum to cover the visible space.
Then, when scrolling to an edge (top or bottom), append or prepend new views on demand. Like the message list in any web-based chat app, like messenger.com.
Since you will only have one drop down menu open at a time, maybe you can keep a pointer to the element or index of the element it is attached to, instead of looping through all the menus.

TinyMCE Enable button while in read only mode

I have a TinyMCE 4.x instance where the text should be in read only mode. But I still have some buttons that I want to have enabled. For example, one button could provide a character count for the part of the text I've selected.
But when I turn on read only mode for TinyMCE all buttons are disabled. Can I enable just my buttons while still retaining read only mode?
It's probably too late for you but other people may pass by here.
I came up by writing this function
function enableTinyMceEditorPlugin(editorId, pluginName, commandName) {
var htmlEditorDiv = document.getElementById(editorId).previousSibling;
var editor = tinymce.get(editorId);
var buttonDiv = htmlEditorDiv.querySelectorAll('.mce-i-' + pluginName.toLowerCase())[0].parentElement.parentElement;
buttonDiv.className = buttonDiv.className.replace(' mce-disabled', '');
buttonDiv.removeAttribute('aria-disabled');
buttonDiv.firstChild.onclick = function () {
editor.execCommand(commandName);
};
}
It does the trick in 2 steps:
make the button clickable (remove mce-disabled CSS class and remove the aria-disabled property)
assign the good command to the click event
And in my editor init event I call the function.
editor.on('init', function () {
if (readOnly) {
editor.setMode('readonly');
enableTinyMceEditorPlugin(htmlEditorId, 'preview', 'mcePreview');
enableTinyMceEditorPlugin(htmlEditorId, 'code', 'mceCodeEditor');
}
});
Current version of TinyMCE for which I wrote this code is 4.4.3. It may break in a future version, specifically about the selectors to get and modify the good HTML elements.
Command identifiers can be found at this page otherwise you can also find them under tinymce\plugins\PluginName\plugin(.min).js
Here is a simple way to enable your custom toolbar button and attach a click event handler inside a read only TinyMCE editor using JQUERY:
//Initialize read only Tinymce editor so that Lock button is also disabled
function initReadOnlyTinyMCE() {
tinymce.init({
selector: '#main'
, toolbar: 'myLockButton'
, body_class: 'main-div'
, content_css: 'stylesheets/index.css'
, readonly: true
, setup: function (readOnlyMain) {
readOnlyMain.addButton('myLockButton', { //Lock button is disabled because readonly is set to true
image: 'images/lock.png'
, tooltip: 'Lock Editor'
});
}
});
}
function displayReadOnlyTinyMCEwithLockButtonEnabled() {
var edContent = $('main').html();
$("#main").empty();
initReadOnlyTinyMCE(true);
tinyMCE.activeEditor.setContent(edContent);
//enable the lock button and attach a click event handler
$('[aria-label="Lock Editor"]').removeClass("mce-disabled");
$('[aria-label="Lock Editor"]').removeAttr("aria-disabled");
$('[aria-label="Lock Editor"]').attr("onclick", "LockEditor()");
}
function LockEditor() {
alert("Tiny mce editor is locked by the current user!!");
//Write your logic to lock the editor...
}
I couldn't find an easy way to do this. The simplest way is to remove the contenteditable attribute from the iframe body instead and substitute a read only toolbar set. It also means that people will still be able to copy content from the editor.
$("iframe").contents().find("body").removeAttr("contenteditable");
How about this :
editor.addButton('yourButton', {
title: 'One can Enable/disable TinyMCE',
text: "Disable",
onclick: function (ee) {
editor.setMode('readonly');
if($(ee.target).text() == "Disable"){
var theEle = $(ee.target).toggle();
var edit = editor;
var newBut = "<input type='button' style='opacity:1;color:white; background-color:orange;' value='Enable'/>";
$(newBut).prependTo($(theEle).closest("div")).click(function(e){
edit.setMode('design');
$(e.target).remove();
$(theEle).toggle();
});
}
}
});
You can try to run the code below:
$("#tinymce").contentEditable="false";
if you have more than one editors, you can use their id like below
$("#tinymce[data-id='idOfTheEditor']").contentEditable="false";

YUI3 - Update CSS class based on XY position

I am using the drag and drop to be able to move one of my nodes within a page. What I want to be able to do is once the drag and drop is completed, get the XY position and if it is outside a certain position (XY), then the class applied to the node should be updated.
http://jsfiddle.net/gabrielesandoval/ab1cjrcj/
CSS:
.dd-demo-inside {
background-color: #8DD5E7;
color: #000;
}
.dd-demo-outside {
background-color: #004C6D;
}
JS:
YUI().use('dd-constrain', function(Y) {
var dd1 = new Y.DD.Drag({
node: '#dd-demo-1'
}).plug(Y.Plugin.DDConstrained, {
constrain2node: '#dd-demo-canvas1'
});
});
So in the JS fiddle example above, if the box moves to the outer color, then the CSS applied to "dd-demo-1" should change from .dd-demo-inside to .dd-demo-outside.
I know YUI has a getXY() function but I wasnt sure how the best way to use it or what event it can be used on to make sure it is called once the dragging of the node is completed.
Any help you can provide would be much appreciated.
You can use the Node.inRegion method to test if the node is inside another node, passing true as the second parameter will ensure that it is fully inside the target region.
http://jsfiddle.net/ab1cjrcj/16/
YUI().use('dd-constrain', function(Y) {
var dragNode = Y.one('#dd-demo-1'),
innerCanvasNode = Y.one('#dd-demo-canvas3'),
dd1;
dd1 = new Y.DD.Drag({
node: dragNode
}).plug(Y.Plugin.DDConstrained, {
constrain2node: '#dd-demo-canvas1'
});
dd1.on('drag:end', function(e){
if (dragNode.inRegion(innerCanvasNode, true)){
dragNode.replaceClass('dd-demo-outside', 'dd-demo-inside');
} else {
dragNode.replaceClass('dd-demo-inside', 'dd-demo-outside');
}
//console.log(dragNode.inRegion(innerCanvasNode, true));
});
});
So i updated my own code. I subscirbed to the drag:end event--
dd1.on('drag:end', getOffsetTop);
Then I make a pure JS function that just checks for the offsetTop and offsetLeft. I think I should be able to create my own condition based on these values to change the class names.
Once you release you can call a function that uses javascript to retrieve the X and Y offset and update the class accordingly. Assuming you're using jQuery it could look something like this.
var offset = $(element).offset();
if(offset.left > x && offset.top > y) {
element.addClass(newClass);
}
I built onto your JSFiddle to show how it would work http://jsfiddle.net/ab1cjrcj/12/

jQuery custom plugin - setting private options for multiple instances

Hi folks!
I'm currently developing a client project where I saw myself doing the same javascript code over and over again. So I though it would be useful to wrap the logic inside a custom jQuery plugin. I've achieved it for a single instance of the plugin, but for multiple instances, I think I'm having a problem with the properties of each instance overwriting each other.
Well, let's get to the code! Here is the currently code that I have for the plugin:
// RESPONSIVE MENU ===========================//
// wrapper for a responsive menu plugin, //
// made by Favolla Comunicação //
//============================================//
/* INSTRUCTIONS
Apply the plugin on the main wrapper of the responsive menu. For example:
$(#menu).responsiveMenu($(#trigger));
The plugin just toggles the classes, leaving the effects and layout for the css
CONFIG
- trigger: the selector of the button that will activate the menu (required)
- activeClass: class name to be injectet when the toggle is activated (default: active)
- submenuTrigger: the selector of the buttons that will activate the submenus, if the menu will have another levels (default: $('sub-toggle'))
- submenu: the selector of the submenus (default: $('.submenu'))
- submenuActiveClass: class name to be injected on the submenus when they are activated (default: open)
- breakpoint: max window whidth where the plugin will work (default: 720)
- timeOut: time in milissegundos to limite the onResize repeat. (default: 100)
- moveCanvas: option to activate the "off canvas" pattern or not (just puts a class on the main elements of the page). (default: false)
- canvas: class name of the elements that build the "canvas" (default: null)
*/
;(function ( $, window, document, undefined ) {
$.fn.responsiveMenu = function(settings){
var config = {
'trigger': '',
'activeClass': 'active',
'submenuTrigger': $('.sub-toggle'),
'submenu': false,
'submenuActiveClass': 'open',
'breakpoint': 720,
'timeOut': 100,
'moveCanvas': false,
'canvas': '',
};
if (settings){$.extend(config, settings);}
// plugin variables
var mTrigger,
menu = $(this),
active = config.activeClass,
button = config.trigger,
bpoint = config.breakpoint,
submTrigger = config.submenuTrigger,
submenu = config.submenu,
submenuActive = config.submenuActiveClass;
canvasOn = config.moveCanvas;
canvas = config.canvas;
time = config.timeOut;
return this.each(function () {
if($(window).width() > bpoint){
mTrigger = false;
} else {
mTrigger = true;
}
onChange = function(){
clearTimeout(resizeTimer);
var resizeTimer = setTimeout(function(){
if($(window).width() > bpoint){
mTrigger = false;
menu.removeClass(active);
button.removeClass(active);
if(canvasOn){
canvas.removeClass(active);
}
} else {
mTrigger = true;
}
}, time);
}
$(window).bind('resize',onChange);
$(document).ready(onChange);
button.click(function(){
if(mTrigger) {
menu.toggleClass(active);
button.toggleClass(active);
if(canvasOn){
canvas.toggleClass(active);
}
}
});
if(submenu){
var submenuClass = '.' + submenu.prop('class');
// toggle for the submenus
submTrigger.click(function(){
if(mTrigger) {
if($(this).hasClass(active)){
submTrigger.removeClass(active);
submenu.removeClass(submenuActive);
} else {
submTrigger.removeClass(active);
$(this).addClass(active);
submenu.removeClass(submenuActive);
$(this).next(submenuClass).addClass(submenuActive);
}
}
});
}
});
}
})( jQuery, window, document );
And then, when I want to apply the plugin, I make like this:
$('#menu-wrapper').responsiveMenu({
trigger: $('#nav-toggle'),
submenu: $('.submenu'),
submenuTrigger: $('.submenu-toggle'),
moveCanvas: true,
canvas: $('.canvas'),
breakpoint: 862
});
$('#search').responsiveMenu({
trigger: $('#search-toggle'),
breakpoint: 862
});
The main issue here is when I set to instances of the responsiveMenu();, it seems like some options are overwriting. For example, the first instance set moveCanvas to true, and it works, but when I leave it blank for the second instance (which leaves the moveCanvas option set to false for this element, this options for the first instance don't work anymore.
I know that maybe I'm not following the jQuery plugin best pratices, and I even read something about the jQuery Boilerplate, which looks great, but I'm not an advanced javascript developer, so there a lot of things that I could do better, but I just don't now how to do.
Anyway, any help with this issue (and opinions about the plugin) will be very welcome!
var mTrigger,
menu = $(this),
active = config.activeClass,
button = config.trigger,
bpoint = config.breakpoint,
submTrigger = config.submenuTrigger,
submenu = config.submenu,
Until here you were doing correct.
submenuActive = config.submenuActiveClass;
canvasOn = config.moveCanvas;
canvas = config.canvas;
time = config.timeOut;
Then, you introduced a semicolon - which leads to the further assignments (canvasOn, canvas and time) not being part of the var statement any more. They're no variable declarations, you assign to global variables here - and that way you overwrite the settings of the first plugin instance.
Change every but the last semicolon to commata.
You need to encapsulate your settings in a class or closure and store them for each element the plugin is called on.
return this.each(function () {
...
$(this).data('some-key', settings);
...
});
If you'd like to learn about jQuery plugin authoring, they have an article on it here http://learn.jquery.com/plugins/basic-plugin-creation/

IE javascript error - possibly related to setAttribute?

I am using Safalra's javascript to create a collapsible list. The script works across several browsers with no problem. However, when I apply the javascript to my own list, it fails to act as expected when I use IE (I'm using 7 at the moment). It simply writes the list, without the expand and contract images.
I copied the Safalra's javascript precisely, so I assume the error must be in my own list. This is how I generated my list:
<body onLoad="makeCollapsible(document.getElementById('libguides'));">
<ul id="libguides">
<script type="text/javascript" src="http://api.libguides.com/api_subjects.php?iid=54&more=false&format=js&guides=true&break=li"></script>
</ul>
(Yes, I do close the body tag eventually.) When I run this in IE, it tells me that line 48 is causing the problem, which appears to be:
node.onclick=createToggleFunction(node,list);
Here's the entire function:
function makeCollapsible(listElement){
// removed list item bullets and the sapce they occupy
listElement.style.listStyle='none';
listElement.style.marginLeft='0';
listElement.style.paddingLeft='0';
// loop over all child elements of the list
var child=listElement.firstChild;
while (child!=null){
// only process li elements (and not text elements)
if (child.nodeType==1){
// build a list of child ol and ul elements and hide them
var list=new Array();
var grandchild=child.firstChild;
while (grandchild!=null){
if (grandchild.tagName=='OL' || grandchild.tagName=='UL'){
grandchild.style.display='none';
list.push(grandchild);
}
grandchild=grandchild.nextSibling;
}
// add toggle buttons
var node=document.createElement('img');
node.setAttribute('src',CLOSED_IMAGE);
node.setAttribute('class','collapsibleClosed');
node.onclick=createToggleFunction(node,list);
child.insertBefore(node,child.firstChild);
}
I confess I'm too much of a javascript novice to understand why that particular line of code is causing the error. I looked at some of the other questions here, and was wondering if it might be a problem with setAttribute?
Thanks in advance.
Edited to add:
Here's the code for the createToggleFunction function. The whole of the script is just these two functions (plus declaring variables for the images).
function createToggleFunction(toggleElement,sublistElements){
return function(){
// toggle status of toggle gadget
if (toggleElement.getAttribute('class')=='collapsibleClosed'){
toggleElement.setAttribute('class','collapsibleOpen');
toggleElement.setAttribute('src',OPEN_IMAGE);
}else{
toggleElement.setAttribute('class','collapsibleClosed');
toggleElement.setAttribute('src',CLOSED_IMAGE);
}
// toggle display of sublists
for (var i=0;i<sublistElements.length;i++){
sublistElements[i].style.display=
(sublistElements[i].style.display=='block')?'none':'block';
}
}
}
Edited to add (again):
Per David's suggestion, I changed all instances of setAttribute & getAttribute...but clearly I did something wrong. IE is breaking at the 1st line (which is simply the doctype declaration) and at line 49, which is the same line of code where it was breaking before:
node.onclick=createToggleFunction(node,list);
Here's the first function as written now:
function makeCollapsible(listElement){
// removed list item bullets and the sapce they occupy
listElement.style.listStyle='none';
listElement.style.marginLeft='0';
listElement.style.paddingLeft='0';
// loop over all child elements of the list
var child=listElement.firstChild;
while (child!=null){
// only process li elements (and not text elements)
if (child.nodeType==1){
// build a list of child ol and ul elements and hide them
var list=new Array();
var grandchild=child.firstChild;
while (grandchild!=null){
if (grandchild.tagName=='OL' || grandchild.tagName=='UL'){
grandchild.style.display='none';
list.push(grandchild);
}
grandchild=grandchild.nextSibling;
}
// add toggle buttons
var node=document.createElement('img');
node.src = CLOSED_IMAGE;
node.className = 'collapsibleClosed';
node.onclick=createToggleFunction(node,list);
child.insertBefore(node,child.firstChild);
}
child=child.nextSibling;
}
}
And here's the second function:
function createToggleFunction(toggleElement,sublistElements){
return function(){
// toggle status of toggle gadget
// Use foo.className = 'bar'; instead of foo.setAttribute('class', 'bar');
if (toggleElement.className == 'collapsibleClosed') {
toggleElement.className = 'collapsibleOpen';
toggleElement.src = OPEN_IMAGE;
} else {
toggleElement.className = 'collapsibleClosed';
toggleElement.src = CLOSED_IMAGE;
}
// toggle display of sublists
for (var i=0;i<sublistElements.length;i++){
sublistElements[i].style.display=
(sublistElements[i].style.display=='block')?'none':'block';
}
}
}
Internet Explorer (until version 8, and then only in best standards mode) has a very broken implementation of setAttribute and getAttribute.
It effectively looks something like this:
function setAttribute(attribute, value) {
this[attribute] = value;
function getAttribute(attribute, value) {
return this[attribute];
}
This works fine iif the attribute name matches the property name, and the property takes a string value.
This isn't the case for the class attribute, where the matching property is className.
Use foo.className = 'bar'; instead of foo.setAttribute('class', 'bar');
node.onclick=createToggleFunction(node,list);
That is probably not what you want. Does createToggleFunction return a function? If it doesn't, then I bet you meant this:
node.onClick = function() { createToggleFunction(node, list); };
If my guess is right then the way you have it will set the onClick event handler to be the result of createToggleFunction, not a function like it needs to be.

Categories

Resources