25

I have a DOM situation that looks like this:

  • A is an ancestor of B, which is in turn an ancestor of C

  • Initially, A has styles that inherit to B and C

  • I wish to temporarily highlight B by giving it a highlighted class

    ...however...

  • I want to "escape" the highlighting on C so it changes as little as possible

It seems this is not possible within the cascading paradigm of CSS. The only way to "un-apply" a style is to apply an overriding style. My problem is that the highlight code is in a plugin that wants to play well with an arbitrary page's existing CSS...my selectors are like this:

/* http://www.maxdesign.com.au/articles/multiple-classes/ */
.highlighted.class1 {
    background-color: ...
    background-image: ...
    ...
}
.highlighted.class2 {
   ...
}
/* ... */
.highlighted.classN {
   ...
}

Background is a tricky one...because although it is not an inherited CSS property, changing an ancestor's background can alter a descendant's appearance (if they have transparent backgrounds). So it's an example of something that causes the kind of disruption I'm trying to negate. :-/

Are there any instances of how people have suppressed the inheritance of changes in this fashion? Perhaps using tricks such as caching computed styles? I'm looking for any default technique that can be at least a little smarter than a function-level hook that says "hey, the plugin highlighted this node...do what you need to visually compensate."


UPDATE I have created a basic JsFiddle of this scenario to help make the discussion clearer:

http://jsfiddle.net/HostileFork/7ku3g/

Community
  • 1
  • 1
  • 4
    I know what you're asking, but I have to point out that [`background` is not](http://www.w3.org/TR/CSS2/colors.html#propdef-background) an [inheritable property](http://www.w3.org/TR/CSS2/cascade.html#inheritance). – thirtydot Nov 10 '11 at 17:40
  • @thirtydot - ah darnit, I actually knew that...although the mechanics of this and what it can and can't do is rather new to me. Thanks for pointing it out, helps refine the phrasing of what I actually want... – HostileFork says dont trust SE Nov 10 '11 at 17:50
  • This is sometimes helpful: http://www.suzyit.com/tools/specificity.php – Diodeus - James MacFarlane Nov 10 '11 at 19:44
  • Regarding `.class1`, `.classN`, etc., are they classes that are intended to reflect classes from "arbitrary" pages' styles, or are they purely related to your plugin styling--that is, are you adding the class `.highlighted` to some page class `.class1` or are you adding both classes `.highlighted` and `.class1` to elements purely for your plugin purposes? – ScottS Nov 11 '11 at 02:20
  • @Scott I'm trying to make a very generic plugin. It can either scan the page and add "highlightability" to existing class1...classN attributes (which were literally on the HTML), or you can use a plugin call on specific elements in typical jQuery fashion to call out elements that you want the classes to be added to. So really we would assume that the page did not style with highlighted, class1...N prior to its awareness of the plugin...anything it does with those are assumed to be part of trying to cooperate. – HostileFork says dont trust SE Nov 11 '11 at 17:55
  • Okay, so you are saying that the user of the plugin is in control of creating the CSS styling as you rough out above ("anything it does with those are assumed to be part of trying to cooperate")? The reason I ask is that, if they are in control of that, then what is the big issue with them just "resetting" child elements to whatever they do not want to affect, like `.highlighted.class1 > *` to reset a highlight to all children? Now, if you are generating the CSS, I see your need more clearly. – ScottS Nov 12 '11 at 00:36
  • 1
    What does your "highlight" look like? It's difficult to know what to suggest without more information. A [jsFiddle test case](http://jsfiddle.net/) of what you have so far would be awesome. – thirtydot Nov 13 '11 at 10:17
  • Are you looking for a CSS solution or a javascript solution? – bozdoz Nov 13 '11 at 19:32
  • @thirtydot Excellent suggestion, I've added a JsFiddle to it. What I'm doing is I'm trying to subtly watermark things based on license but have the watermarking suppressed if there is embedded content to which the license does not apply. The idea is to only show this watermark when a "substantial" amount of content is selected by the user. http://jsfiddle.net/HostileFork/7ku3g/1/ – HostileFork says dont trust SE Nov 13 '11 at 20:58
  • @bozdoz I'd be happiest if within CSS there was an easy way to get the desired effect. But anything that makes it more likely that the plugin can be turnkey (even if it's JavaScript and even if it only works in some percentage of cases) is better if it makes the effect less disruptive. – HostileFork says dont trust SE Nov 13 '11 at 21:01
  • Something like this? http://jsfiddle.net/7ku3g/2/ (Note, I'm actually wondering if this is an approximation, not necessarily an answer. Also, there is [lesscss](http://lesscss.org/).) – Jared Farrish Nov 13 '11 at 21:03
  • Or, maybe set a background on everything so the background doesn't bleed through: http://jsfiddle.net/7ku3g/6/ – Jared Farrish Nov 13 '11 at 21:06
  • @JaredFarrish That's the general idea, but bear in mind I'm trying to find a solution that's likely to work for dropping this on an arbitrary page (as opposed to a solution where one has to rewrite the original page's CSS). I'd not heard of lesscss, nor do I see how to apply it here, but that is an interesting thing. To make it "trickier" I've put a background image on the A div in the fiddle...and just imagine the general goal is to keep C looking as close to how it did before the highlight: http://jsfiddle.net/HostileFork/7ku3g/15/ – HostileFork says dont trust SE Nov 13 '11 at 21:16
  • At least with the example you're showing may be flawed in that you're wanting a boundary around elements that are "stacked". See: (**UPDATED**) http://jsfiddle.net/7ku3g/19/ You're seeing the background in your examples simply because the child elements don't have a background set, so they're "transparent" and show the background of the element "below" them in the stack. Get around this by specifying the background (styles) more specifically for the descendent elements. – Jared Farrish Nov 13 '11 at 21:22
  • If you can do without IE7/8 support, [Jan Kuča's](http://stackoverflow.com/questions/8083701/workaround-for-lack-of-css-feature-to-suppress-inherited-styles-and-backgroun/8114880#8114880) answer is very good. – thirtydot Nov 16 '11 at 00:35

9 Answers9

4

If I understand correctly, you want to cut a "hole" in the background of an ancestor. The only way I can think of is using <canvas> to cut the hole and then use the resulting image as the background of the ancestor.

Here is my code: http://jsfiddle.net/7ku3g/24/

I used the JS Fiddle logo as the watermark because you cannot retrieve contents of a canvas with cross-origin resources drawn to it.

Update: You would also want to listen to resize events to update the background as the window size changes.

J. K.
  • 8,268
  • 1
  • 36
  • 35
  • 2
    Updated with the same images as in the question and `resize`: http://jsfiddle.net/thirtydot/7ku3g/42/ – thirtydot Nov 16 '11 at 00:41
  • Cool...I was actually going to ask if you'd mind updating it with a data URI. So far I think this is the most "interesting" answer...and although it probably creates images too large for my specific purposes, it's good to see a thinking-outside-the-box solution instead of "it's impossible"...! This applies to backgrounds, which is how my examples are written, yet I was also hoping not to bake in the idea of what kind of CSS was used to make the highlight (which is why I threw in the red text, as an example of the kind of thing a client might throw in...) – HostileFork says dont trust SE Nov 16 '11 at 02:36
  • If you want to keep other original styling, you could probably call `window.getComputedStyle(c)` before the highlight and keep it by explicitly setting CSS text of the element. That way, you override the inherited styling after the highlight. Do you know what I mean or should I write a simple fiddle? – J. K. Nov 17 '11 at 08:51
  • Though I've accepted the answer from Ricardo Tomasi, I'm awarding you the bounty for the interesting approach...any further fiddling you want to do to make the sample more viable for people who might want to use the approach (turning off the watermark, handling resizes) would probably help people who find this in the future! – HostileFork says dont trust SE Nov 20 '11 at 00:38
  • Thanks! I think that the best solution would be a combination of the two approaches. – J. K. Nov 20 '11 at 09:44
2

As thirtydot already pointed out, the background is not an inherited property but it rather just shines true as the child elements are transparent by default. So really the only way to make them mimic the behavior you want is to set them to the background(-image/color) that is either applied to parent (= div#a) of the highlight element or even below that. In this simple example that is:

.highlight div {
    background-color: white;
}

But in your real website this might be less easy to do.

Marco Miltenburg
  • 6,123
  • 1
  • 29
  • 29
  • What I'd like ideally is somehow to make the effect "automatic" (such as by climbing the page to find the computed styles and generate the compensating CSS?)...so it "just worked", as opposed to having the plug-in user do this all manually. Any technique that makes it turnkey. I've added a background to the sample to show it being more like a real-world situation, and just wondering if there's any way to ease the user's pain... http://jsfiddle.net/HostileFork/7ku3g/ – HostileFork says dont trust SE Nov 13 '11 at 21:44
  • @HostileFork - Have you seen [this question](http://stackoverflow.com/questions/1004475/jquery-css-plugin-that-returns-computed-style-of-element-to-pseudo-clone-that-el)? You could also specify that certain classes should be used, or detect elements that don't have compatible styles. But all of these use Javascript. – Jared Farrish Nov 13 '11 at 21:51
  • @HostileFork I understand what you want but I think it can't be done. – Marco Miltenburg Nov 13 '11 at 22:03
2

"Background is a tricky one because it effectively acts inherited when it is not. But it's still an example of something that causes the kind of disruption I'm trying to negate. :-/"

Incorrect. A, being the highest level element (the oldest ancestor) has a background. When subsequent elements are descended from A (e.g. B and C), they have a background of "transparent", which makes them appear to have the inherited background (but is why your tiled backgrounds show up as contiguous).

We could, as proposed by @bozdoz use a specified value for the sub element in the situation that the highlight is applied, as such:

.highlight.bClass #c {
  background-image: url('http://hostilefork.com/shared/stackoverflow/parchment.jpg');
  background-repeat: repeat;
  color: initial;
}

This would give the illusion of the original background peering through, and, given your example, appears nearly flawless to the naked eye. This however has the unfortunate side effect of being entirely non-dynamic. For an example, we might instead create a new class:

.preventHighlight {
  background-image: url('http://hostilefork.com/shared/stackoverflow/parchment.jpg');
  background-repeat: repeat;
  color: initial;
}

And, in addition to this, add a second line to your javascript:

$('#toggleHighlight').click(function(){
  $('#b').toggleClass('highlight');
  $('#c').toggleClass('preventHighlight');
});

Not bad. But what if it were, for instance, #x that we didn't want highlighted? I propose we give #toggleHighlight a new attribute, name. In the html, try:

<button id="toggleHighlight" name="x">Toggle Highlight</button>

Where name="x" is set to the ID of the element to which we wish to apply the preventHighlight class. Thus, we are able to write the function more akin to:

$('#toggleHighlight').click(function(){
  $('#b').toggleClass('highlight');
  $('#'+$('#toggleHighlight').attr('name')).toggleClass('preventHighlight');
});

You could continue this on, instead creating a function taking two parameters, such as tglHighlight(idToHighlight, idToPrevent) were you so inclined.

edit:

I appear to have misunderstood @bozdoz and done essentially the same thing, without noticing the visible shift in background. I would propose then a slight alternative which includes a correction for the offset of the background. This is dependent solely upon a minor change to my proposed javascript:

$('#toggleHighlight').click(function(){
  $('#b').toggleClass('highlight');
  $('#'+$('#toggleHighlight').attr('name')).toggleClass('preventHighlight');
  $('.preventHighlight').css('background-position', function(index) {
    var ch_pos = $('.preventHighlight').position();
    var a_pos = $('#a').position();
    var x = a_pos.left-ch_pos.left;
    var y = a_pos.top-ch_pos.top;
    console.log( x+"px "+y+"px" );
    return x+"px "+y+"px";
  });
});
stslavik
  • 2,920
  • 2
  • 19
  • 31
  • Well I meant with my statement that "the effects of setting a background become inherited, even though background is not itself an inherited property"...but I reworded it again. The idea of using a background position shift looks very handy and I wonder if chasing up the hierarchy and finding computed background styles could hint one to knowing where to throw in compensation, automatically? – HostileFork says dont trust SE Nov 19 '11 at 18:23
  • I've done it before. You can use a loop and the `parent` to get the offsets easily enough. `.position().left` on its own will give you the left offset, so you could reduce a few lines easily. – stslavik Nov 22 '11 at 18:31
2

I have a solution that helps with non-background related styles. You can see the fiddle http://jsfiddle.net/7ku3g/83/ which is only a roughed concept. Some key points: 1) it is imperative that the prepended styleControl element is added to the inner most containing element. For example, you had the .highlight class added to the .bClass div but there was another full wrapping element, the blockquote within that. 2) You would want to remove the styleControl element upon removal of the highlight class.

In general, the idea is to add an invisible styling element first in the inner most wrapper, then use the general sibling selector ~ and :not selector to style further down elements. Again, this does not solve your background issues, but does seem to be a way to help solve true inheriting properties since those styles are NOT applied to the wrapper div, but to the newly created first sibling in the group and the properties applied based off that sibling only to those they need to be applied to.

One issue with this technique is any "naked text" (see http://jsfiddle.net/7ku3g/84/) does not then get styled. So it is not a whole solution to your issue. EDIT: For such cases, grabbing and adding a carefully crafted span around those naked nodes might work. Something similar to: Style a certain character in a string.

CSS

.highlight.bClass  {
    background-image: url('http://hostilefork.com/shared/stackoverflow/cc-by-nc-nd.png');
    background-repeat: repeat;
}

.highlight.bClass .styleControl {
    width: 0;
    height: 0;
    position: absolute;
}

.highlight.bClass .styleControl ~ *:not(.cClass) {
    color: red;
}

UPDATE: Some further explanation based on HostileFork's comments. First, my solution here only matters if there is a child element that you want to prevent from inheriting styles (actual inheritable styles) you are adding. Second, multiple may need to be added depending on how deep things are nested. Here's some pseudocode html to discuss:

<div> (containing licensed material)
    <childA1> (this is the wrapping blockquote in your example; could be anything)
       <scriptAddedStyleElementB0>
       <childB1>content<endchildB1> (apply styles)
       <childB2>content<endchildB2> (don't apply styles)
       <childB3> (don't apply styles because a child is not having styles applied)
          <scriptAddedStyleElementC0>
          <childC1> (apply styles)
            <!-- noScriptAddedStyleElementD0 -->
            <childD1>content<endchildD1>
            <childD2>content<endchildD2>
          <endchildC1> 
          <childC2>content<endchildC2> (don't apply styles)
       <endchildB3>
    <end-childA1>
<end-div1>

You are targeting the outermost div to apply the highlight class to but certain children you don't want affected. Note how A1 and B3 don't contain any content (text/images) themselves, they just contain other elements. Those are "wrappers" in my use of the term (they are just used to group their children, though they may have styling themselves).

To answer your question of "when the delving needs to stop," it is when you reach a point where you no longer care about styles inheriting. In the example above, 2 styling elements would need to be added, one at B0 and one at C0 but not at D0. This is because the general sibling selector ~ only applies to elements that have the same parent and that follow that sibling. So B0 is going to apply the styles to B1 but not B2 (non-licensed) or B3 (contains a non-licensed). Element C0 is going to apply the styles to C1 (and its licensed children D1 and D2) but avoid styling C2. The content of B2 and C2 would still be inheriting the original styles from the wrapping div (C2 by way of B3), while B1, C1, D1, D2 would all get the licensing styling.

The difficulties in this are: 1) earlier versions of IE (7 and 8) do not recognize some of this css, specifically the :not selector. That might potentially be worked around. 2) my previous note about "naked text" or content. If B3 looked like:

 <childB3> 
    <scriptAddedStyleElementC0>
    Some unwrapped text
    <childC1>...<endchildC1>
    Some other unwrapped text and an <img> tag
    <childC2>...<endchildC2>
    More unwrapped text
 <endchildB3>

If B3 unwrapped text was needing to be styled for licensing (turned red in your example) then you would need to locate all the text nodes via javascript and wrap them in span elements to apply the styling, because if you apply the style to B3, then it will inherit to C2 which you don't want.

There is no doubt that what you are looking for is difficult to achieve. It would have been impossible via CSS prior to the general sibling selector coming about, and it may get easier with CSS4 standards that are being discussed (being able to style parent elements from children might be leveraged for this situation).

Community
  • 1
  • 1
ScottS
  • 71,703
  • 13
  • 126
  • 146
  • This looks very tricky...I must admit CSS and its ramifications are something I haven't spent enough time with to absorb the advantage of this added node. Some of the terminology you're using (like "wrapper") confuses me, so I'll mention I didn't use blockquote for any particular reason. I'm talking about a plugin that could supply a watermark to certain divs on a page when a "substantial" amount of text is selected, and then be able to "escape" that...imagine trying to cooperate with minimal modifications to a page like http://www.snopes.com/rumors/wingdings.asp – HostileFork says dont trust SE Nov 18 '11 at 17:54
  • Yes, I understand what you are going for. My solution is for your problem of inheritable properties. If you look at your html the `.bClass` div wraps (contains) all your child elements. But also, the way your demonstration is constructed, you have a `blockquote` also wrapping (containing) the other two child divs. For my solution to work, the injected styling element needs to be inserted in the inner most parent element that wraps the children (in your case, the `blockquote`). This is because it must be the first sibling of the direct parent of the elements you are styling. – ScottS Nov 18 '11 at 18:14
  • I'm not sure what it means to say I'm styling the elements in the blockquote but not the blockquote. Let's say for the sake of argument that I had a blockquote with another blockquote in it...would I need to seek down to the inner one? For what reason? To me the only thing aggregating the elements I am styling is their containership in the div, so I'm curious about what this delving deeper buys and when the delving needs to stop... – HostileFork says dont trust SE Nov 18 '11 at 18:34
1

Manage all CSS changes in javascript so that they're easily reversible. As you said, it's not possible in pure CSS, but you can cache the styles via javascript.

In steps:

  1. define what kind of elements will be ignored inside the highlight
  2. check if you have a background-image in any of the parents
  3. if you do, save the image and that element's offset()
  4. grab the descendant elements
  5. save their current CSS styles (before the highlight)
  6. apply the highlight styles to the target
  7. re-apply the saved styles for the descendants
  8. if there was a parent with a background image, calculate and offset the image accordingly

This keeps all styles for the inner elements intact, and accounts for background images that "fall through" transparent backgrounds.

Here's a test: http://jsfiddle.net/W4Yfm/2/

This leaves a messy sea of style attributes behind though, you might want to clean it up. There are probably unnacounted edge cases too.

To be most reliable you should use a technique like @Jan pointed out (draw the highlight on canvas), but you won't have support in IE < 9, and it's probably too slow for mobile.

Ricardo Tomasi
  • 34,573
  • 2
  • 55
  • 66
  • This is pretty much along the lines of what I was thinking, although I was wondering what other approaches there might be. I wasn't aware of the `background-position` but that fills in a missing link in the strategy. It seems like something that could get hammered out to work with most cases, and I appreciate you taking the time to write it! – HostileFork says dont trust SE Nov 19 '11 at 18:43
0

You could add .highlight .cClass to the top two css groups, and set color:initial on .highlight .cClass, like so:

.aClass, .highlight .cClass {
    background-image: url('http://hostilefork.com/shared/stackoverflow/parchment.jpg');
    background-repeat: repeat;
}

.aClass, .bClass, .cClass, .xClass, .highlight .cClass {
    border : 2px solid #000;
    margin: 20px;
    padding: 20px;
}

.highlight .cClass {
    color: initial;
}

The only problem, in this example, is that the background-position changes. But it's the best I've come up with.

bozdoz
  • 12,550
  • 7
  • 67
  • 96
0

Check it out: http://jsfiddle.net/t63m7/

This will work in your particular example case in jsFiddle. You would have to play around with background-position in your real code perhaps.

Trick in this case was to add the following CSS:

.highlight.bClass div {
    background-image: url('http://hostilefork.com/shared/stackoverflow/parchment.jpg');
    background-repeat: repeat;
    background-position: -84px 0;
}
Strelok
  • 50,229
  • 9
  • 102
  • 115
0

I would set the background of .cClass to be the same as .aClass with css, but If you want them to match up you will have to change the background position style as well.

Mikey G
  • 3,473
  • 1
  • 22
  • 27
0

Hmm, a lot of suggestions but my first thought which I have not tested would be to give the the same tags as whatever highlight you're doing to B to the C class and make it !important.

So:

.cClass div { 
background-image: none !important; 
}
awm
  • 2,723
  • 2
  • 18
  • 26
  • I use jsfiddle all the time, but I have found that there is a lot of nuinces that it misses. It's always 100% on. So something that doesn't work in jsfiddle does work in browers (maybe not all top three - but you can get it to work with a hack). – awm Nov 20 '11 at 19:38