[css-break][css-overflow-3][css-regions][css4-ui] generalizing 'region-fragment' into a fragmentation primitive: a first step to solving multi-line ellipsis

Warning: this is a long mail, so go grab some coffee. It should of interest
to people who care about the "last line ellipsis" problem, but also about
fragmentation (including pagination and multicol), regions, and
overflow:fragments / overflow:paged. (If that doesn't get me an audience, I
don't know what will :)

We have an old resolution to add last line ellipsis as a value to
text-overflow (or do something similar), but it doesn't say how it would
work.

http://lists.w3.org/Archives/Public/www-style/2009Nov/0219.html

This isn't the first time this is brought up, because showing an ellipsis at
the end of a multi line piece of content is a real problem that needs solving,
but doing it as a value of text-overflow is misguided, as that property is
fundamentally about inline overflow, and is ill equipped to deal with this use
case.

If you have 200px of vertical space to display some arbitrary mark up, and
want to show an ellipsis if it ends on overflowing, what you have is not an
inline overflow issue.
- What is the last line anyway? The last line that doesn't overflow at all? The
  last line that is readable, even if it overflows a little bit? How about if
  the line doesn't overflow, but its text shadow does?
- What do you do for content that isn't inline text? inline-block may be
  fairly easy to deal with, but what about floats? flexbox? abspos?

What we're dealing with is a fragmentation problem, and treating it as such
will bring us much closer to the solution that trying to shoe-horn it into
text-overflow. Fragmentation is a difficult problem, but one that the group is
committed to solving (and has already done to a large extent).

In this mail, I am proposing:
- extensions/generalizations to some existing properties and pseudos relating
  to fragmentation to make a more unified model
- suggestions on how to build on top of that to support multi-line ellipsis

Everything is of course open to bikesheding.

== 1 - Generalize 'region-fragment' ==

'region-fragment'
(http://dev.w3.org/csswg/css-regions/#the-region-fragment-property) currently
only does anything on the last region of a region chain. But what it does is
much more generally applicable. On a block container element that has too much
content to fit, 'region-fragment:break' will cause the element to behave like
a fragmentainer.

So let's rename ‘region-fragment’ into ‘fragmentation’, move it somewhere else
(probably the fragmentation spec) as it does not have depend on much of the
regions spec, and give it a definition along these lines:

Name:       fragmentation
Value:     auto | break
Initial:    auto
Applies to: block containers
Inherited:  no

On things that are naturally fragmentainers, (pages, columns, non-last regions
in a region chain...), 'auto' lets them be fragmentainers as they normally
are.

On things that are not, 'auto' gives the usual behavior: overflowing content is
processed according to the 'overflow' property.

'break' causes the element to become a fragmentainer. Content that follows
the break is not rendered (or goes into the next region in the chain if there
is one). All the things that can be used in a fragmentation context
(break-before, break-after, widows, orphans, box-decoration-break...) can be
used.

(spoiler: later in this mail, I extend/generalize this property further)

== 2 - Generalize 'max-lines' ==
Let 'max-lines' (http://dev.w3.org/csswg/css-overflow/#max-lines) apply to any
fragmentainer. There is no reason to restrict it to any particular kind. 

== 3 - Adding the ellipsis ==
This is the least definitive part of this mail.

One possible approach would be to do something similar to text-overflow. With
the following syntax: 
  fragmentation: auto | break [ ellipsis <string> ]?

After layout, remove enough characters and atomic inlines, pushing them
further in the fragmentation context if any, from end of the last line box of
the normal flow before the fragmentation break, to make room for the ellipsis
string to be inserted. If any float or abspos was anchored in the portion that
got removed, they get removed as well. Then, insert the ellipsis or <string>
adjacent to the line end. If it doesn't fit despite emptying that line box,
clip it to what fits.

With what we have so far, we solve the common cases of multi-line ellipsis
with rules as simple as these:

article.preview {
  fragmentation: break ellipsis;
  max-lines: 3;
}

This should work well in the case of textual flows, possibly with atomic
inline elements. It also has a predictable, and hopefully reasonable behavior
if there are floats or abspos elements anchored in that content. On the other
hand, it fails to deal with cases where the fragmentation break occurs in
monolithic content or things like flexboxes which don't have line boxes, and
unless we can come up with a good solution for that, the ellipsis value should
probably be made into a no-op for these cases. Also, while the behavior seems
reasonable to me in the case where the fragmentainer is not part of (or is the
last of) a fragmentation context, it is not so nice otherwise, as the
fragmentation break is no longer at the best point.

In the rest of this mail I am assuming this syntax, but other points should be
valid even if you disagree with this part.

An alternative would be to do something similar to what the regions spec
proposes as a processing model for ::after. Having 'fragmentation: break
block-ellipsis' on a element which is overflowing (and therefore causing
fragmentation) would insert a ::block-ellipsis pseudo element formatted as a
block container, whose height reduces the available height in the
fragmentainer. This is similar to what Tab proposed here:
http://lists.w3.org/Archives/Public/www-style/2012Jul/0688.html
This has the advantage of working with any layout system, and
is well suited for inserting sentences like "Continued on the next page...".
On the other hand, it does not support just adding "..." at the end of the
last line. However, the previous suggestion can, and both can be offered as
alternatives to authors to pick from.

== 4 - 'overflow:fragment' and 'overflow:paged' ==
Both 'overflow:fragment' / 'overflow:paged' and 'fragmentation:break' cause a
regular element to become a fragmentainer. The only difference is what happens
to the content left over after fragmenting. This suggest that they should be
rolled up in a single property, and 'fragmentation' is just right for this job.
Moreover, Figure 5 in the region spec gives a good explanation of how
'overflow: visible' or 'overflow: hidden' interact with 'fragmentation:break'
interact, while the overflow spec has issues (Issue 13/14) on this topic with
no obvious right answer. These issues go way if we move 'fragment' and 'paged'
to be values of 'fragmentation' (renaming 'fragment' to 'clone'):

fragmentation: auto | [ break | paged | clone ] [ ellipsis <string> ]?

Unifying the models also gives us the benefit that the ellipsis mechanism now
works in each of them.

== 5 - Explaining regular pagination / fragmentation ==
We could also try to use the above as a primitive to explain regular
pagination by having 'auto' compute to 'paged' when applied to a page box.

We could also have an explicit value to turn off fragmentation on things that
would normally fragment, and use it as the computed value of 'auto' on
ordinary elements.

fragmentation: auto | none | [ break | paged | clone ] [ ellipsis <string> ]?

If some values don't make sense in some situations, we can always make then
compute to one that does. For example, 'clone' could computes to 'paged' on
page boxes, while on descendants of elements where 'fragmentation' computes to
'paged', 'paged' could compute to 'none'.

== 6 - Pseudos Elements ==
On an element where 'fragmentation' computes to something other than 'none',
::after (and probably ::before) should have the same processing model as it
has on regions (i.e. it is always a block container).

An ::ellipsis (for 'text-overflow:ellipsis' and 'fragmentation:break ellipsis')
and/or a ::block-ellipsis (for 'fragmentation: break block-ellipsis') pseudo
should be introduced, with the appropriate property restrictions, to provide
extra styling to the inserted text.

== 7 - Pseudo classes ==
Having made the generalization in point 5, fragment styling
(http://dev.w3.org/csswg/css-overflow/#fragment-styling) should probably also
be generalized to apply to all types of fragmentations.

== 8 - What about multicol ==
Column boxes are also fragmentainers, and if we explain fragmentation through
the 'fragmentation' property, one of the values should explain what is
happening to columns.

Currently, we have no selector to get at column boxes, but the pseudo classes
from the previous point could probably be made to apply here as well (and if
not, I expect we will eventually have columns selectors anyway). So we would
need to decide if one of the other existing values can be used to explain
column fragmentation or if we need a new one. Maybe 'clone' can work here,
since we're repeating boxes of the same type.

== 9 - Regions ==
The current regions model works fine under this system. Just define that:
- 'auto' computes to 'none' on the last region of a chain, and to 'break' on
other regions
- overflowing content goes into the next region of the chain if there is
one and the if computed value of 'fragmentation' on the overflowing region is
'break'

Also, if we're ever interested in introducing something similar to regions,
except without naming the flows, we could also have a functional value to the
'fragmentation' property that takes a selector, and puts the overflowing
content there. This seems decently expressive: I've picked a few interesting
examples from the regions spec, and they can be nicely rephrased in these
terms (Filtering examples like example 4 don't do so well). Note that I am not
pushing for this as a complement or an alternative to regions, just noting
that this 'fragmentation' syntax lends itself well to this sort of extension.

ex2:
<article> ...some content... </article>
<aside> ad or image content </aside>
<div class="bottom"></div>

article {
  max-height: 80vh;
  fragmentation: insert-into(#bottom);
}

ex3:
<nav> ...some links... </nav>
<div class="menu">
  <nav></nav>
  ...some more links...
</div>

nav:not(.menu > *) {
  fragmentation: insert-into(.menu nav);
}

ex6:
article {
  column-count: 2;
  flow-from: article;
  height: 6em;
  column-gap: 1em; 
  fragmentation: insert-into(#remainder);
}

<body>
  <article>...</article>
  <div id="remainder"></div>
</body>

== 10 - What about event handling? ==
Handling clicking and other events in the ellipsis definitely has valid use
cases, but this is a general css problem of event handling in pseudo elements,
not something specific to ellipsis. However we solve that will work here.

== 11 - What about generated content? ==
"continued on page 13...", with 13 being dynamically generated, or similar
things, preferably also with links, is a reasonable thing to want in ellipsis
text. This is also a problem that goes beyond ellipsis, and should use a
generic solution. This looks similar enough to the problem solved by
'string-set' and the 'string()' function
(http://dev.w3.org/csswg/css-gcpm/#named-strings), so we should reuse that (if
we keep it at all).

== Conclusion ==
I think a generalization of 'region-fragment' into 'fragmentation', together
with the other related adjustments suggested, would expose a useful primitive,
and help unify and simplify how a number of fragmentation-related features
work, and open up some new behavior.

On top of that, I think this would be a good basis for building multi-line
ellipsis, even though I'll readily admit that this part needs more work.

If anybody has made it this far, I'd love to hear what you think.

 - Florian

Received on Tuesday, 20 January 2015 13:13:27 UTC