Javascript/HTML: How to make a small UI pop out on select - javascript

I am making a chrome extension that interacts with the current page when the user selects some text.
What we want it to do is to make a small window pop up to let the user select within different options.
Something along the lines of this:
http://img-ipad.lisisoft.com/img/1/5/1526-1-pdf-highlighter.jpg
What we have so far is the following:
document.addEventListener('mouseup',boxOption)
function boxOption(){
var yourSelection = window.getSelection();
if (yourSelection!=""){
/* insert popup here */
}
}
Any help would be greatly appreciated

You can implement the popup by inserting an element into the DOM (or revealing an existing element within the DOM) which simply has a "z-index" property that puts it above the other elements. Ex:
// Create a class that encapsulates the menu element
// This particular implementation constructs a new element
// and adds it to the DOM, but you could instead take the
// element as a parameter or have it retrieve an existing element
var PopupMenu = function() {
this.element = document.createElement('div');
this.element.className = 'popup-menu';
document.body.appendChild(this.element);
// ...
// set up event listeners for this element
// ...
};
// The menu is hidden unless it also has the 'enabled' class
PopupMenu.prototype.setVisible = function(isVisible) {
if (isVisible) {
this.element.classList.add('enabled');
} else {
this.elemnt.classlist.remove('enabled');
}
};
And then in your CSS, you could do:
.popup-menu {
display: none;
}
.popup-menu.enabled {
display: block;
/* this just needs to be larger than the z-index of the items it covers */
z-index: 100;
}
I'll leave the rest of the styling/handlers of the menu up to you.

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.

mouseover event propagation issue - Manually propagate

I am implementing an User Interface for a project I'm working on and can be found here : Toobrok
Each time the mouse of the user enters a div, a class is added to this div to highlight it, I use the stopPropagation() method to restrict the highlighting to the div whose z-index is higher (the top div in the z axis).
However, sometimes, my user needs to select an element hidden by another one, when the dimensions of the 2 elements are different, and if the bottom div is larger, he can find some points of the bottom div not hidden by the top one, but when the dimensions are the same, I would like the user to be able to press a key to change the depth (on the z-axis) of his selection.
The relevant code is given below (in CoffeeScript), but a javascript solution would also help me:
Ui.bind = (elements, index) ->
ids = Ui.getIdSelector(elements)
$(ids).attr("centroid", index)
$(ids).mouseover (event) ->
event.stopPropagation()
Ui.highlight $(ids)
$(ids).mouseout (event) ->
event.stopPropagation()
Ui.resetHighlight $(ids)
I hope the question is clear and looking forward to your answer.
This is an example of HTML to consider :
<!DOCTYPE html>
<html>
<head>
<title> Sample page </title>
</head>
<body>
<div id="container">
<div id="child1">Some text...</div>
</div>
</body
</html>
And the related css :
#container {
height: 200px;
width: 500px;
}
#child1 {
height: 90%;
width: 90%;
}
When the mouse enters the child1 element, this element is highlighted, I want the container element to highlight when the user press a specific key.
I could use the JQuery parent() function to select that element on this example, but I am not sure it is a good solution, sometimes, the parent can have a size of 0px and and then a mouseover on this element would not be consistent. I want to select the element normally selected by Javascript if I do not use the stopPropagation() event.
I actually just found something that might help :
How to undo event.stopPropagation in jQuery?
But I cannot use that in my case... Because my condition is another user action, and I cannot synchronously wait for an user to do something.
I started writing code but then decided to leave implementation to you. Here is the text explanation:
At some point of time (probably when user press button to cycle through all hovered elements) you have to find all candidates for highlighting. There is no other way to do it rather than manually loop through all your elements and check if mouse position is inside their bound rect. You can get mouse coordinates from argument in mouseover callback. Save all these hovered elements in some array.
Next, you have to manually choose which element to highlight. Just highlight the first element in saved array and move the element to the end of array. You also may want to increase this element z-index and add callback for mouseout to this element.
Hope it helps, feel free to ask if you need more details.
You could use the CSS property pointer-events to make the child insensitive. Then events will be targeted to the element displayed below. For simple highlighting you should use pure CSS, however, jQuery can be helpful not to highlight the parent element as well while child is hovered without Ctrl.
Some example (also uploaded to JSFiddle, click into the output pane to make it responsive for keyboard events):
<div id="container1" class="container">
<div id="child1" class="child">Some text...</div>
</div>
div { border:1px dashed red; } /* for demo */
.container
{ height: 200px;
width: 500px;
}
.child
{ height: 90%;
width: 90%;
}
.insensitive
{ pointer-events:none;
}
.container:hover:not(.no-hilight),
.child:hover
{ background-color:yellow;
}
/* other color for demo */
.child:hover{ background-color:green; }
// make events passthrough child when <Ctrl> is held down
$(document).on('keydown keyup', function(ev) {
if (ev.key === 'Control') // for performance
$('.child')[ev.ctrlKey?'addClass':'removeClass']('insensitive');
});
// don't hilight container when child is hovered
$('.child').on('mouseover', function(ev)
{
$('.container').addClass('no-hilight');
});
// never suppress hilight when container is hovered directly
$('.container').on('mouseover', function(ev)
{ if(ev.target === ev.currentTarget)
$('.container').removeClass('no-hilight');
});
// just test which element a click is targeted to
$(document).on('click', function(ev)
{ console.log('click:', ev.target);
});
var preId = 0;
function makeBlack(id)
{
if(id)
{
$('#'+id).css('border-color','black');
}
}
function makered(id)
{
$('#'+id).css('border-color','red');
}
$(document).ready(function(){
$('div').mouseout(function() {
var currentid = this.id;
makeBlack(currentid);
preId = currentid;
});
$('div').mouseleave(function() {
var currentid = this.id;
makeBlack(currentid);
preId = currentid;
});
$('div').mouseover(function() {
var currentid = this.id;
makeBlack(currentid);
makered(preId);
preId = currentid;
});
$('div').mouseenter(function() {
var currentid = this.id;
makered(currentid);
preId = currentid;
});
});
Have you tried something like this for the CSS?
#container.hover{
height: 200px;
width: 500px;
//add a background-color to that element since its a div element
//background-color: (colour)
}
i should hope that the div element would automatically highlight the container div with whichever color you have selected

Checking if Element hasClass then prepend and Element

What I am trying to achieve here is when a user clicks an element it becomes hidden, once this happens I want to prepend inside the containing element another Element to make all these items visible again.
var checkIfleft = $('#left .module'),checkIfright = $('#right .module');
if(checkIfleft.hasClass('hidden')) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.hasClass('hidden')) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
I tried multiple ways, and honestly I believe .length ==1 would be my best bet, because I only want one element to be prepended. I believe the above JS I have will prepend a new element each time a new item is hidden if it worked.
Other Try:
var checkIfleft = $('#left .module').hasClass('hidden'),
checkIfright = $('#right .module').hasClass('hidden');
if(checkIfleft.length== 1) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.length== 1) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
else if(checkIfleft.length==0){
$('.resetLeft').remove()
} else if (checkIfright.length==0){
$('.resetRight').remove()
}
Basically if one element inside the container is hidden I want a reset button to appear, if not remove that reset button...
hasClass() only works on the first item in the collection so it isn't doing what you want. It won't tell you if any item has that class.
You can do something like this instead where you count how many hidden items there are and if there are 1 or more and there isn't already a reset button, then you add the reset button. If there are no hidden items and there is a reset button, you remove it:
function checkResetButtons() {
var resetLeft = $('#left .resetLeft').length === 0;
var resetRight = $('#left .resetRight').length === 0;
var leftHidden = $('#left .module .hidden').length !== 0;
var rightHidden = $('#right .module .hidden').length !== 0;
if (leftHidden && !resetLeft) {
// make sure a button is added if needed and not already present
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if (!leftHidden) {
// make sure button is removed if no hidden items
// if no button exists, this just does nothing
$('#left .resetLeft').remove();
}
if (rightHidden && !resetRight) {
$('#right').prepend('<span class="resetRight">Reset Right</span>');
} else if (!rightHidden) {
$('#right .resetRight').remove();
}
}
// event handlers for the reset buttons
// uses delegated event handling so it will work even though the reset buttons
// are deleted and recreated
$("#left").on("click", ".resetLeft", function() {
$("#left .hidden").removeClass("hidden");
$("#left .resetLeft").remove();
});
$("#right").on("click", ".resetRight", function() {
$("#right .hidden").removeClass("hidden");
$("#right .resetRight").remove();
});
FYI, if we could change the HTML to use more common classes, the separate code for left and right could be combined into one piece of common code.
Add the reset button when hiding the .module, if it's not already there :
$('#left .module').on('click', function() {
$(this).addClass('hidden');
var parent = $(this).closest('#left');
if ( ! parent.find('.resetLeft') ) {
var res = $('<span />', {'class': 'resetLeft', text : 'Reset Left'});
parent.append(res);
res.one('click', function() {
$(this).closest('#left').find('.module').show();
$(this).remove();
});
}
});
repeat for right side !
I've recently experimented with using CSS to do some of this stuff and I feel that it works quite well if you're not trying to animate it. Here is a jsfiddle where I can hide a module and show the reset button in one go by adding/removing a 'hideLeft' or 'hideRight' class to the common parent of the two modules.
It works by hiding both reset button divs at first. Then it uses .hideLeft #left { display:none;} and .hideLeft #right .resetLeft { display: block; } to hide the left module and display the reset button when .hideLeft has been added to whichever element both elements descend from. I was inspired by modernizr a while back and thought it was a neat alternative way to do things. Let me know what you think, if you find it helpful, and if you have any questions :)

CKEditor default style for styleCommand (format styles with buttons)

I have created a CKEditor plugin which does the base p, h2, h3, h4 formatting with custom buttons (instead of the stylescombo). It works great, but if I uncheck an element (ex. 'h2'), sets the 'div' tag as parent element for the row. I want to be the 'p' as the default element and also the 'p' button can't be unchecked (unless I clicking on another, ex. 'h2' button). How is this possible?
The plugin looks like:
CKEDITOR.plugins.add('stylesbuttons_custom',{
lang:'en',
icons:'p,h2,h3,h4',
init:function(editor){
var order=0;
var addButtonCommand=function(buttonName,buttonLabel,commandName,styleDefiniton){
if (!styleDefiniton)
return;
var style=new CKEDITOR.style(styleDefiniton);
editor.attachStyleStateChange(style,function(state){
!editor.readOnly && editor.getCommand(commandName).setState(state);
});
editor.addCommand(commandName,new CKEDITOR.styleCommand(style));
if (editor.ui.addButton){
editor.ui.addButton(buttonName,{
label:buttonLabel,
command:commandName,
toolbar:'basicstyles,'+(order+=10)
});
}
};
var lang=editor.lang.stylesbuttons_custom;
addButtonCommand('P',lang.p,'p',{element:'p'});
addButtonCommand('H2',lang.h2,'h2',{element:'h2'});
addButtonCommand('H3',lang.h3,'h3',{element:'h3'});
addButtonCommand('H4',lang.h4,'h4',{element:'h4'});
}
});
I load the plugin like:
config.extraPlugins='stylesbuttons_custom';
I put buttons to toolbar like:
config.toolbar:[['P','H2','H3','H4','Pre']];
Here is a screenshot about the problem:
Cross posting my answer from CKEditor forum.
I think that you need to write your own command instead of using CKEDITOR.styleCommand.
It should work exactly like CKEDITOR.styleCommand when style is not yet applied on current selection.
But when clicked again it should apply the paragraph style, not remove the previously applied style. E.g:
styleCommand.prototype.exec = function( editor ) {
editor.focus();
if ( this.state == CKEDITOR.TRISTATE_OFF )
editor.applyStyle( this.style );
else if ( this.state == CKEDITOR.TRISTATE_ON )
editor.applyStyle( paragraphStyle );
};
PS. I created a ticket: http://dev.ckeditor.com/ticket/10190 because I think that removing block style should revert back to paragraph (in enterMode=P). For now use the above workaround.
Yes, #Reinmar notifies that there is an error in the CKEditor's style.js where the this._.enterMode is not defined.
Doing this on style.js, resolve the problem:
this._ = {
definition: styleDefinition,
enterMode: CKEDITOR.config.enterMode
};
And from now on when a style button is unchecked the block changes to the default 'p' element.
Now my complete working plugin looks like:
(function(){
CKEDITOR.plugins.add('custombuttons',{
lang:'hu,en,de,ro',
init:function(editor){
var order=0,t=this,lang=editor.lang.custombuttons;
// addButtonCommand helper
var addButtonCommand=function(buttonName,buttonLabel,commandName,styleDefiniton){
var style=new CKEDITOR.style(styleDefiniton);
var styleCommand=function(style){
this.style=style;
this.allowedContent=style;
this.requiredContent=style;
this.contextSensitive=true;
};
styleCommand.prototype={
exec:function(editor){
editor.focus();
if (this.state==CKEDITOR.TRISTATE_OFF)
editor.applyStyle(this.style);
else if (this.state==CKEDITOR.TRISTATE_ON)
editor.removeStyle(this.style);
if(commandName!='fakecommand'){editor.execCommand('fakecommand');editor.execCommand('fakecommand');} /* hack to change button state properly */
},
refresh:function(editor,path){
this.setState(path&&this.style.checkApplicable(path)?(this.style.checkActive(path)?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF):CKEDITOR.TRISTATE_DISABLED);
}
};
editor.addCommand(commandName,new styleCommand(style));
if(editor.ui.addButton){editor.ui.addButton(buttonName,{label:buttonLabel,command:commandName,toolbar:'basicstyles,'+(order+=10),icon:t.path+'images/'+commandName+'.png'});}
};
// _fakebutton (hack)
addButtonCommand('_fakebutton','','fakecommand',{element:'span'});
// style buttons
addButtonCommand('P',lang.p,'p',{element:'p'});
addButtonCommand('H2',lang.h2,'h2',{element:'h2'});
addButtonCommand('H3',lang.h3,'h3',{element:'h3'});
addButtonCommand('H4',lang.h4,'h4',{element:'h4'});
addButtonCommand('Pre',lang.pre,'pre',{element:'pre'});
addButtonCommand('Mini',lang.mini,'mini',{element:'p',attributes:{class:'mini'}});
addButtonCommand('Important',lang.important,'important',{element:'span',attributes:{class:'important'}});
addButtonCommand('Comment',lang.comment,'comment',{element:'span',attributes:{class:'comment'}});
addButtonCommand('Mark',lang.mark,'mark',{element:'mark'});
addButtonCommand('ImgLeft',lang.imgLeft,'imgLeft',{element:'img',attributes:{class:'imgleft'}});
addButtonCommand('ImgRight',lang.imgRight,'imgRight',{element:'img',attributes:{class:'imgright'}});
addButtonCommand('ImgCenter',lang.imgCenter,'imgCenter',{element:'img',attributes:{class:'imgcenter'}});
// button shortcut keys
editor.setKeystroke(
[
[CKEDITOR.CTRL+48,'p'], // Ctrl+0
[CKEDITOR.CTRL+49,'h2'], // Ctrl+1
[CKEDITOR.CTRL+50,'h3'], // Ctrl+2
[CKEDITOR.CTRL+51,'h4'], // Ctrl+3
]);
}
});
})();
There is still a hack in the code. I needed to run a 'fakecommand' to really update (refilter?) the changed tags and all its parent tags. For example the 'p.mini' button caused problems (the state was not updated) when clicked multiple time. So there is still an unelegant solution. Any idea how to force to update or refilter the code after a style is applied?

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