Wednesday, November 23, 2022

javascript translate urls into links, but skipping things that are already links...

A while ago I posted a snippet of linky, and explaining how "changing the current highlighted text into a link" (heuristically figuring out if text to be made clickable or the URL itself) made more sense to me (for use on the backend of my blog) than either markdown-like approaches or dialogs with two input boxes.

But with my work flow, sometimes it was annoying to have to highlight the link in order to linkify it, and I realized a better UX would be "if I click "linky" and there is no text selected, change every https:// starting URL looking thing into a link." 

This stackoverflow was a good start, but assumed any URL wasn't already in a link - like if you ran it twice (or there were links elsewhere) you'd get <a href="<a href=" crap. 

But the regular expression to do it properly was tough. I got some help and ended up with (with a few tester function to start... and pardon the bad pre code ;-) )

function testLinkify(){

   console.log('starting test'); 

   test(`https://foo.com`, `<a href="https://foo.com"></a>`);

   test(`\nhttps://foo.com`,`\n<a href="https://foo.com"></a>`);

   test(`<a href="https://foo.com"></a>`,`<a href="https://foo.com"></a>`);

   test(`<a href="https://foo.com"></a> https://bar.com`,`<a href="https://foo.com"></a> <a href="https://bar.com"></a>`);

     test(`https://foo.com\nhttps://bar.com <a href="https://baz.com">BAZ</a>`,`<a href="https://foo.com"></a>\n<a href="https://bar.com"></a> <a href="https://baz.com">BAZ</a>`);

}

function test(input,expect){

  const testFunction = linkifyBareHttp;

  const output = testFunction(input);

  console.log (output === expect ? 'PASS':'FAIL');

      console.log(` INPUT: ${input}`);

  if(output !== expect) {

    console.log(`EXPECT: ${expect}`);

    console.log(`OUTPUT: ${output}`)

  }

}

 


function linkifyBareHttp(inputText){

    //URLs starting with http://, https://, or ftp://

    const replacePattern1 = /\b(?<!(\'|\"))(((https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]))/gim;

    return inputText.replace(replacePattern1, '<a href="$2"></a>');

}

    

testLinkify();


You can play with that at a codepen

(UPDATE: it was funny playing with my admittedly idiosyncratic UI, starting to use it for real. Turns out I had a workflow that expected to be able to click "linky" before typing anything and just get a place holder "<a href>". Also in theory I might hit linky before any textarea has the focus (it uses the rule of "less touched textarea is the 'current' one") so I special cased a few in the offchance I had started focusing yet... anyway...)     

Wednesday, November 16, 2022

container queries for dummies, the complete idiot's guide to moving beyond media queries

So a lot of sites use CSS media queries to switch between mobile and desktop looks, but sometimes you're more concerned about how much room your widget actually has to work with, rather than what the device itself is.

Enter "Container Queries" - fairly wide support but caniuse points out Firefox is lagging (elsewhere claimed its under way)

I found the MDN pages on container-queries a bit confusing, though (especially how it uses "container" both for a class name even though it's part of the "@container" syntax...(

If you had a media query like 

@media (max-width: 600px) {
  /* … */
}

You might try to replace it with 

@container (max-width: 600px){
/* … */
}

The catch is: some ancestor of the content you are actually concerned about resizing has to act as a "containment context"  - some element with the css property 
 container-type: inline-size;
(inline-size is basically width; block-size could be used for height, or size for both. But I think nine times out of ten you're looking for inline-size there)

Here is a minimalist example codepen I made. Ignore the border and padding I put in for reference...  Ignoring the ugly colors and the border and padding for the wrappers... it demonstrates how the context element can have its own width or be constrained by its own parent, etc.

PS Remember the "_____ for Dummies" books? They started with DOS for Dummies... probably it would have been kinder if the title was "for complete newbies" or "with few assumptions about what you already know", but then the alliteration wouldn't have worked...

nothing is forever. even on the internet.


Thinking of "Twitter 2" and my sudden disinclination to rely on their "embed" feature; I wonder how committed they will be to preserving the viability of "legacy" links and embeds... I mean that's part of what let twitter be at the headwaters of other media, it's paradigm was the quotable soundbite, and that quoting could be done EITHER by copy paste (usually with a link) or embed.

And I see that with all kinds of video I've embedded on my blog, especially video (both youtube videos gone private, and then entire services like Vine gone away)

That kind of linkrot makes me sad. Just reminds me of the old school optimism of how "URLs should be forever", like that somehow the (on paper) possibility of endless, perfect duplication meant the brave new "Information Superhighway" of the Internet would be proof against entropy and organizational decay. 

Well, to quote the old Usenet (remember that?) group alt.folklore.computers: "We're all soldiers in the war against entropy."

never rebuild from scratch

Ha, with Twitter's current instability I guess I should quote this tweet from @GergelyOrosz directly:

Scoop: Elon Musk just sent an email to all staff outlining "Twitter 2.0", writing it will"need to be extremely hardcore". Long hours, high intensity.

People need to click "yes" to confirm being part of this by 5pm ET tomorrow, else they get 3 months severance. More details:

It made me think of Joel Spolsky Things You Should Never Do, Part I where he says never "build it up / rewrite it again from scratch" (Netscape was the precursor to Mozilla/Firefox)

Monday, November 14, 2022

get all properties of the event object...

 Playing around with mouse/touch/pointer events, I was a annoyed that Object.keys(e) only returns "isTrusted" when there's lots of good information there... 

In short there's all kinds of "own" (meaning not inherited from a parent object) and"enumerable" aspects messing things up, but short answer is that this stackoverflow suggests 

function getAllPropertyNames (obj) {
    const proto     = Object.getPrototypeOf(obj);
    const inherited = (proto) ? getAllPropertyNames(proto) : [];
    return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
}

and has some other more specific functions as well.

Thursday, November 10, 2022

sticky headers for sub-parts of page in react

 We had a request to have a sticky header for a sub-part of our page, so that as you scrolled down through a long job listing you would always see the "apply" button. (this is on a complex page with job listing cards on the left and the current job being viewed on the right.)

Frankly we overthought; looked at competitor sites, some of them were playing games with changing the heights of parent containers, or letting the big button header scroll up  but then pushing in a smaller mini version of the header. We thought the solution might need Intersection Observer, or maybe switch to "position:fixed" under conditions or some such, but I think we found something pretty cheap and cheerful that does the job. 

The trick is using "useRef" to get the dom element for the (position: relative) header, getting the parent of the "current" ref, using getBoundingClientRect() on the header and the parent to do the appropriate positioning math, and useEffect to set up and tear down the event listner.

End result for the Proof of Concept:

  const headerRef = useRef<HTMLDivElement>(null)

  const adjustHeaderPosition = (headerRef?: HTMLDivElement | null) => {
    if(! headerRef) return;
    const headerRect = headerRef?.getBoundingClientRect();
    const headerHeight = headerRect?.height || 0;
    const parentRect = headerRef?.parentElement?.getBoundingClientRect();
    const parentTop = parentRect?.top || 0;
    const parentHeight = parentRect?.height || 0;
    
    if(parentTop !== undefined && parentTop < 0) {
      let topLocation = parentTop * -1;
      if(topLocation + headerHeight > parentHeight) {
        topLocation = parentHeight - headerHeight;
      }
      headerRef.style.top =`${topLocation}px`;
    } else {
      headerRef.style.top =`0px`;
    }
  }

  useEffect(() => {
    function onScroll() {
      const header = headerRef?.current;
      adjustHeaderPosition(header);
    }
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <JobViewHeaderContainer>
    <JobViewHeaderContainer ref={headerRef}>

(the code is a little careful so as to avoid lint "this could be null!" warnings)

So it makes sure the bottom of the header doesn't go beyond the bottom of parent, and the overall effect is decent.

Thursday, November 3, 2022

click to copy to clipboard widget via javascript

On phones (at least iPhone, probably on Android), copy and pasting is a fiddly operation. (Heck I remember when the first iPhones didn't even have copy and paste, took them a while to get the loupe ui for that right)

One convenient widget I've seen adds a "click-to-copy" widget, like here's one for branch name in git:

I whipped up a version at jsitor.com but it was a little easier to share this codepen....

Here's the JS:

const makeCopyToClipboardWidgets = () => {
  document.querySelectorAll(".click-to-copy").forEach((el, i) => {
    const widget = document.createElement('a');
    widget.innerHTML = '⧉';
    widget.className = 'click-to-copy-widget';
    widget.addEventListener("click", () =>{
      const text = el.tagName === 'A' ? el.href : el.innerText
      navigator.clipboard.writeText(text);
      widget.classList.add('active');
      setTimeout(() => { widget.classList.remove('active');}, 500)
    });
    el.after(widget);
  });
}
document.addEventListener('DOMContentLoaded', makeCopyToClipboardWidgets);

And a little CSS flavoring:

.click-to-copy {
  border: black 1px dashed;
}
.click-to-copy-widget {
  cursor: pointer;
  position: relative;
  top: -4px;
  font-weight: bolder;
  font-size: 1.2em;
}
.click-to-copy-widget.active {
  color: red;
}

So then any DOM object of class "click-to-copy" will get a border and a little icon to click:

(pardon my developer junkbox content)

Some details:
  • I dug around for the Unicode U+29C9, "Two Joined Squares" for the icon
  • I add a little "active" class and the remove it half a second later to provide some visual feedback
  • after I finished the first version, I realized a common usecase is when you want to copy a link - especially useful because it's all to easy to accidentally follow the link as you're trying to copy it on a phone. But sometimes link have a different caption than the content that is worth copying (leaving out "https://", for instance, to make it cleaner). So my code special cases A tags, and grabs the href for them instead of the text...
So, pleased with the outcome. "Vanilla JS" solutions work well for me, because there's a lot on my sites that don't have a node-ish build process... but I admit i still think a bit in terms of jQuery, so that You Might Not Need JQuery page is still a godsend, even though I haven't used jQuery in a while... it was just such a clean syntax for element selection, adding classes, and putting stuff on "ready"...