DEV Community

Bernd Wechner
Bernd Wechner

Posted on

Bringing it all together: Copy With Style

And now the tour is done, all that is left is to present the Copy With Style interface and how it is used.

We expose a single class Copy_With_Style that can be instantiated as follows in client side Javascript. For example:

const clipboard = new Copy_With_Style({ button: document.getElementById("button_to_copy"),
                                        element: document.getElementById("element_to_copy"),
                                        stylesheets: ["default.css"],
                                      }); 
Enter fullscreen mode Exit fullscreen mode

To clarify, this is yet more delectably clear and intuitive JavaScript notation (not)! It is the de facto standard all the same, for passing a list of optional, named arguments to a function (in this case, a class constructor).

It works using a JavaScript object which is an arbitrary container for attributes and is described well enough by others.

Important to note is only that in reality there is only one argument, which is an object and can be written like so in Javascript:

const myobj = {}
Enter fullscreen mode Exit fullscreen mode

Looks a little like a Python dictionary to me but hey in JavaScript it's an object but looks and works the same way more or less as a dictionary. That is, it can contain properties and be initialized as follows:

const myobj = {prop1: val1, prop2: val2, prop3: val3}
Enter fullscreen mode Exit fullscreen mode

JavaScript is a little more flexible here and you could write also:

const myobj = new Object();
myobj.prop1 = val1;
myobj.prop2 = val2;
myobj.prop3 = val3;
Enter fullscreen mode Exit fullscreen mode

What you need to know is that the single object argument that Copy_With_Style takes can have these attributes (default values are shown and if missing are mandatory arguments, or attributes or properties or whatever you like to call them):

Argument Default value Description
button None an HTML element that can be clicked. Ideally a button element. If it has a progress element as a child or sibling this can be used for monitoring progress on style in-lining. Unnecessary unless you are copying very large HTML elements with many 10s or 100s of thousands of children.
element None an HTML element that will be copied ()with all its children) to the clipboard when button is clicked.
stylesheets [] An array of strings that represent CSS style sheets. For example ["default.css", "extras.css"]. If this is a non empty array then only styles from these sheets will be included in the copy. if you know your element only draws styles from specific sheets, then specifying them can speed up the style inlining and/or shrink the size of the copy. Otherwise all the stylesheets the document includes will be used.
mode "attribute" A string. Either "attribute" or "tag".
If "tag" then a <style> tag is added to the copy and the element's style attributes are left untouched. This is fast and can conserve pseudo elements like :hover. Most email clients can't cope with this, but it will produce a rich copy in HTML contexts that do.
If "attribute" then the style attributes of element and all its children will be updated with style information taken from the stylesheets and the browsers computed styles, to produce as true a copy as possible. This produces a larger copy generally than "tag" but is respected by most email clients today. It's also a lot slower to produce if your element is large enough canhave significant processing costs.
defer [50000,0] Meaningful only if mode == "attribute" and determines if and how often the style in-liner will defer to to UI to keep the UI responsive. Possible values are:
true: defer to the UI after ever element is processed. Not recommended, slows down processing immensely.
false: never defer to the UI while in-lining. Will lock the UI until finished. No problem for small elements, can be bothersome for very large elements.
[threshold, frequency]: The UI is deferred to only if more than threshold elements are being copied, and only once ever frequency elements are processed. If frequency is 0 and a progress bar is specified, it is is optimised to be number of elements per progress bar pixel
triggers ["button"] Meaningful only if mode == "attribute" and determines how and when style in-lining is triggered. This is an array of triggers and can contain:
"button": to request that style in-lining happen when the copy button is clicked.
"schedule": to schedule a style in-lining once the DOM is fully rendered.
"observe": to request that element be observed, and if it's seen to change, then a style in-lining will be triggered. This is useful if element is responsive to user interactions. If defer is set to maintain a responsive UI any change to element will trigger a request for any existing in-lining to bail and start one anew.
Sensible combinations are:
["button"] for small and moderate elements.
["schedule", "observe"] for extremely large elements.
progress false Meaningful only if mode == "attribute" and requests that a progress bar be displayed to convey the progress of style in-lining. Accepts the following values:
false: no progress bar is used.
true: a progress bar is used if an HTML progress element is found as a sibling or child of element.
an HTML progress element: specify an element if you prefer and it will be used.
If a progress bar is being used then defer is also set, to [0, 0] if it is not set (false) or the threshold is set to 0 if it is an Array of 2 elements. This is necessary because without a deferral to UI the progress bar will not update (render).
copy_wrapper true element is wrapped in a simple <div> with id copy_me_with_style before styling (either by tag or attribute as specified by mode). If true the wrapper will be place on the clipboard, if false only its contents will be (i.e element and any style information added).
class_button "copy_with_style" The CSS class assigned to the provided button. This is the buttons rest state though it conserves this class across all states. When clicked in this state the button will trigger a copy preparation if necessary and a copy of prepared data to the clipboard.
class_preparing "preparing_for_copy" The CSS class assigned to the provided button when copy preparation is in progress. This may be very very quick (near instantaneous) or take some while, depending on choice of mode and size of the element. Most things are very fast, but "attribute" mode with very large elements can be slow. When in this state the button will either be disabled or trigger a restart of the preparation depending on configuration.
class_ready "ready_to_copy" The CSS class assigned to the provided button when copy preparation is in complete and a copy is ready to place on the clipboard. When in this state the button will simply copy the prepared texts and HTML to the clipboard.
deep_exclusions null Meaningful only if mode == "attribute" and provides a function to call, which accepts an HTML element as its only argument, and returns true if that element and all its children should be excluded from the copy.
The default implementation excludes all hidden (not visible) elements.
If provided, this function replaces the default implementation.
shallow_exclusions null Meaningful only if mode == "attribute" and provides a function to call, which accepts an HTML element as its only argument, and returns true if that element and and only that element should be excluded from the copy (its children are grafted onto the parent).
The default implementation excludes all all A tags that link internal to the site (href begins with /) and DIV tags that have the class "tooltip".
If provided, this function replaces the default implementation.
extra_deep_exclusions null Identical to deep_exclusions, except that it augments rather than replaces the default implementation.
extra_shallow_exclusions null Identical to shallow_exclusions, except that it augments rather than replaces the default implementation.
debug false If true, debugging information will be written to the console. Useful for checking the scheduling and observation and copy event triggers and such. Was used in developing and tuning this little class and remains in place for future use.
log_performance false If true will log style in-lining performance to the console. This was used to arrive at the performance statistics discussed above.
log_HTML_to_console false If true will log the styled HTML to the console, where it can be inspected. Useful for debugging if pasting brings no joy.
log_text_to_console false If true will log the styled text to the console, where it can be inspected. Useful for debugging if pasting brings no joy.
check_clone_integrity false When adding styles element is cloned and it is this clone that is styled and added to the clipboard. true request that after cloning its integrity is checked. It's never failed, and there's no reason it should, and this is unlikely to be of any great use.
classes_to_debug [] An array of CSS class names. If specified will break in the browser debugger during style in-lining when an element with one of the named classes is being processed. A nice way to drill down to specific classes to inspect the JavaScript variables if for any reason in-lining is not producing joy for a given class.
styles_to_debug [] An array of style names. If specified will break in the browser debugger during style in-lining when an element with one of the named styles being applied is being processed. A nice way to drill down to specific styles to inspect the JavaScript variables if for any reason in-lining is not producing joy for a given class. if classes_to_debug are defined will break if both a class and style match are found. This can of course easily be tuned in code as needed.

Conclusion

And that brings this 12 part series to a conclusion, having introduced a small JavaScript class/library, Copy With Style:

https://github.com/bernd-wechner/Copy-with-Style/blob/master/README.md

It is currently 857 lines, albeit unminimised runs at 35kB thanks to what is hopefully clean documented code. It does drop to 16kB when minimised and comments all stripped:

https://www.minifier.org/
https://html-cleaner.com/js/

though few minimisers I tried cope with the class definition (bizarrre).

I hope it finds some use. After the survey of existing options failed to provide a sensible one for a client side Copy button, I was stuck writing one and it was a journey.

Discussion (7)

Collapse
lionelrowe profile image
lionel-rowe

To clarify, this is yet more delectably clear and intuitive JavaScript notation (not)!

There's plenty of unintuitive stuff about JS, but I've never before heard anyone claim passing options objects to functions was part of it. What's unintuitive about it? How is it inconsistent with how objects and functions usually work in JS? What's the more intuitive alternative?

Collapse
thumbone profile image
Bernd Wechner Author

Good question. Glad you asked.

I find it deeply unintuitive on a few fronts.

  1. The odd ({ .. }) syntax. Immediately upon encountering it the brain does "a huh?" and is off to search and read up on what this means. On finding it that this is an anonymous object declaration as a the single argument to a function the second reason is encountered (2. below). But this first "huh?" moment wins it the "unintuitive" label in my book I guess, intuitive being things that look familiar and sensible not disorienting and puzzling.

  2. The way that arbitrary objects are use as a dictionary. That jars on first encounter and also engenders this kind of "what?" reaction. Which wins it that label too. I doubt very much this was use case in mind when objects were added to JavaScript and the syntax evolved Much rather I suspect this feature was twisted into use as the most convenient argument passing syntax when many optional, default valued options are involved.

  3. The way JavaScript implements anonymous functions (and object instantiators as a subset of that) with such terse, unclear syntax is also consistently a challenge to new comers and far from intuitive. Most languages in my experience make such anonymous definitions explicit and clear (for example with new, or lambda keywords). But JavaScript instantiates objects and functions generally with deeply unintuitive syntax (the beloved arrow syntax -> is in that category causing I suspect a lot of reading by people when they first see it, and have that "huh?" moment ;-)

A more intuitive syntax here? Easy, Python implements one, simply allow for named arguments with default values. Done. The function call then has the familiar ( ... ) form rather than ({ ... }) and and no anonymous object is instantiated. Much easier to read, learn and simply "get" when first encountered in someone else's code.

Collapse
lionelrowe profile image
lionel-rowe • Edited on

It seems like your main argument is "I know Python and I don't know JS, therefore Python is more intuitive than JS".

The syntax and usage of JavaScript objects is almost identical to those of dictionaries in Python. Python's **kwargs is an additional type of syntax — useful, sure, but if anything less intuitive when you first encounter it. Compare:

Python:

def fn(**options):
    return options.foo

options = { "foo": 5 }

fn(**options)
Enter fullscreen mode Exit fullscreen mode

JS:

function fn(options) {
    return options["foo"]
}

let options = { "foo": 5 }

fn(options)
Enter fullscreen mode Exit fullscreen mode

Does the usage of ** really make the python version more intuitive, imagining for a moment that you don't have prior knowledge of any programming languages?

Thread Thread
thumbone profile image
Bernd Wechner Author

It seems like your main argument is "I know Python and I don't know JS, therefore Python is more intuitive than JS".

Can't agree there. The three observations shared stand. There is simply nothing inviting intuitive about x = myfunc({a: 1}). The ({ ... }) syntax beggars classification as "intuitive" regardless of you programming background (and while I draw a Python comparison because Python and JavaScript sit as two of the most popular languages just for the record I have a fairly heave background with a fair range of languages, off the cuff Basic, C, FORTRAN, Pascal, Ada, Perl, VBA, C# and yes Python, and I'm not feeling particularly Python biased at all).

But yes the syntax can be very very similar to Python (already observed and one reason for the comparison) and yet you never encounter x = myfunc({a: 1}) because like other languages (albeit far from all) it supports named arguments from the outset.

And from this 2. follows, using arbitrary anonymous objects as an argument to simulate that is far from intuitive. Familiarity with named arguments goes back at least to Visual Basic and isn't a Python bias. Arguably a VB bias ;-). But I don't think it a bias in any direction to observe that use of an arbitrary anonymous object as a vehicle to carry arguments flexibly is counter-intuitive, very probably no designed by intent but a community standard that emerged applying object to get a useful outcome.

I should add it's not 'bad'. It's fine, and works, and is a neat trick if anything. It's just not intuitive is all.

I should close though admitting that well intuition is by definition at some level biased, so it's not bias I would deny, so much as a specific one. To turn it around while intuition is a function of bias, it's a broad experience based bias and so unintuitive is almost anything that is novel, specific to particular language and not familiar or reminiscent of experience, or neatly mnemonic - easy to connect with familiar things. Novelty is mostly but not always unintuitive I guess. But I'd reserve that word when the novelty is not a really new idea, so much a peculiar quirk.

Thread Thread
thumbone profile image
Bernd Wechner Author

Does the usage of ** really make the python version more intuitive

I missed that, sorry. But the answer there is no, it doe snot. But one encounters such syntax very rarely in Python indeed. The unpackers * and ** are not intuitive at all now, and require careful explanation to all newcomers. But you can read a LOT of Python code and never encounter them and that includes a LOT of Pyton code with functions that take large numbers of named arguments.

Your more like to see:

def fn(opt1=def1, opt2=def2, opt3=def3)
Enter fullscreen mode Exit fullscreen mode

Though you will at times come across fairly early in Python exposure:

def fn(*args, **kwargs)
Enter fullscreen mode Exit fullscreen mode

and there's nothing intuitive about that no, it needs careful explanation and instruction ;-). One gets familiar with it, it's not hard to learn, but sure as heck ain't easy to divine what it means on first encounter no.

Thread Thread
lionelrowe profile image
lionel-rowe • Edited on

Ahh I see, yeah I'm showing my lack of familiarity with Python here — I only knew about **kwargs, not arg=val.

I'd still argue that named arguments that can be supplied in any order are an additional concept to learn on top of positional arguments, whereas passing an object literal to a function is understandable as long as you understand positional arguments + objects (or call them "dictionaries", "hash maps", etc if that helps nomenclature-wise. JS objects are not Java objects.)

It's true that accepting default values can be a little confusing in JS:

function fn({ foo = 1, bar = 2 } = {}) { /* ... */ }

// or...
const defaults = { foo: 1, bar: 2 }

function fn(opts) {
  const { foo, bar } = { ...defaults, ...opts }
  /* ... */
}
Enter fullscreen mode Exit fullscreen mode

However, the unintuitiveness here is only from the implementer's point of view. The consumer still has a simple and intuitive API they can use:

fn() // supply no arguments, fallback to defaults
fn({ bar: 7 }) // supply an `options` object with partial or full options
Enter fullscreen mode Exit fullscreen mode
Thread Thread
thumbone profile image
Bernd Wechner Author

the unintuitiveness here is only from the implementer's point of view

Spot on! In fact two important distinctions here:

  1. Intuitiveness exists in the user and developer realm completely independently. They are two very different things. And historically developers inability to role play the intuitiveness of a user (use case definitions and such) led to developer-centric UIs.

  2. All intuitiveness is a personal experience and hinges on ones experience base and what, thanks to that, feels familiar and "intuitive" (meaning little more than guessable form the base of familiarity I suspect). It is only then and when someone again is able to role-play the "average" newcomer say to a language that it has any meaning to claim intuitive or not.

On 2. it is simply the case that yes I made the claim (and still would) that for a newcomer to JavaScript with almost nay other or now coding experience the x = y({ ... }) syntax engenders a "huh?" feeling which is my general gauge for "not intuitive". Intuitive features I imagine engendering more of a "oh, yeah" feeling when encountered. They just sit well. But one thing that runs counter to intuition is a pattern such as the exemplified one in which I see one set of braces immediately inside another of a different type. So I was left, reading and learning to try and work what on earth, that actually means. Of course as confessed in the article learning it came with a "Doh!" feeling as I was of course familiar with the basic object instantiation syntax options already, it was ot a discovery, the discovery was that I hadn't noticed this very subtle implicit anonymous (unnamed) object instantiation in a list of arguments.