LOL: Workflowy privacy policy

I’ve been an almost happy workflowy’s user for about a year, and as I wrote earlier , I’m done with workflowy. Yesterday, I read their privacy policy more carefully, and, you know, it’s full of gems, check it out:

Under the 2. Data that we use, receive, collect, process, share or store and how we may use it, they have a few nice points:

2.1.2. We might use, receive, collect, process or store Personal Data on potential customers, customers, employees, service providers, users of the Services and the Website, etc.

2.1.3.8. AS MOST OF THE DATA IS PROVIDED BY THE USER OF THE SERVICES WITHOUT OUR KNOWLEDGE, PROVIDE OTHER CATEGORIES OF PERSONAL DATA, SPECIFICALLY “SENSITIVE” AND “SPECIAL CATEGORIES OF DATA” (AS DEFINED BY GDPR i.e. medical data, financial data, political opinions, religious data, childrens’ data etc ) IS UNDER THE USER’S SOLE RESPONSIBILITY AND OWN RISK AND WE RECOMMEND NOT PROVIDING THIS TYPE OF DATA.

LOL, What kind of data I supposed to keep safe in Workflowy then?

And another one:

2.1.4.3. Internal business: We may use your Personal Data for internal business purposes, including, without limitation, to help us improve our Services, Website content and functionality, to better understand our customers and users, to protect against, identify or address wrongdoing, to enforce our contracts and this Privacy Policy, to provide you with customer service and to generally manage and operate our business (e.g., pay salaries and make considerations).

WTF is “Internal business purposes”, huh?

And this is for all users - even for those who have paid money. Oh my god, they suck so hard.

I know that almost all online web services have similar privacy policies, I just wrote about one of them.


Setup volar v2+ in neovim

Suppose you have Neovim and kickstart installed, you program in vue and typescript, and you want to have all these fancy features that volar offers.

In the ideal world, simply adding volar={} in LSP config should be enough, but starting from volar v 2.0 and higher, typescript support was moved to a separate package - @vue/typescript-plugin.

First of all, we need to install it globally:

npm i -g @vue/typescript-plugin

Then, open the init.lua file, find the code with local servers = {..} and make sure your tsserver config looks like this (add this section if it doesn’t exist):

tsserver = {
  init_options = {
      plugins = {
        {
          name = "@vue/typescript-plugin",
          -- Exact location of the typescript plugin
          location = "/usr/local/lib/node_modules/@vue/typescript-plugin",
          languages = {"javascript", "typescript", "vue"},
        },
      },
  },
  -- Add TS support for vue files
  filetypes = {'vue', 'javascript', 'typescript'}
}

You need to set the location of where the @vue/typescript-plugin is installed. To find out where is it, run this command:

npm root -g

This command prints the path to the directory where global packages are installed (on linux and mac, it’s usually /usr/local/lib/node_modules).

The last step - add the volar server:

local servers = {
  tsserver = {...},
  volar={}
  -- You can also add additional useful
  -- LSP servers, for example:
  -- eslint = {}
  -- tailwindcss = {}
}

That’s it, after restarting neovim, you’ll have the full experience of volar!

Problem solved: Hugo did not update content on linux

Recently I was faced with a weird hugo behaviour on linux - it didn’t update the content of the site after running the dev server.

First thing that I tried is to run hugo with disabled “Fast Render Mode”:

hugo server --disableFastRender

And it didn’t help.

Solution: Reboot linux. I can’t find the link right now, but I found this answer somewhere on the internet. Turned out, it’s all because of suspend in linux - somehow the fact that I wasn’t shutting down a system relates to this issue.

Asciidoctor tutorial

Asciidoctor is a markup language similar to markdown, but more readable.

Installation

Asciidoctor has multiple “backends”, so you can choose from multiple options:

Node

Run

npm i -g asciidoctor

Ruby

Search for a package with name asciidoctor:

apt-get install asciidoctor

How to convert asciidoctor page to html

asciidoctor page.adoc -o page.html

The best feature of Asciidoctor #1

Links are easy to remember, check it out:

This link is parsed automatically:

https://example.com

This link will render as a word 'link':

https://example.com[link]

If you want to open a link in a new tab:

https://example.com[window=_blank]

https://example.com[link^]

Mailto links are also automatically parsed:

mailto:username@example.com

mailto:username@example.com[email me]

The best feature of Asciidoctor #2

Images.

Now you’ll never being thinking whether you wrote a link or an image element.

image::coolpic.png[]

image::coolpic.png[cool picture]

Basic elements

This example should give you the full understanding of the base elements in asciidoctor:

= Asciidoctor tutorial

Asciidoctor is a powerfull and elegand
markup language, much better than markdown.

It's strengths:

- Links
- Images
- Elegant headings syntax


== Text formatting

- **Bold text**
- __Italic text__
- +++Underlined text+++


== Blockquotes

Blockquotes are also better than in markdown:

----
This is a blockquote
----

Authoring is also supported:

[quote, proj11.com]
----
Asciidoctor as a **markup** language is better
than markdown, but its backends suck.
----

Links


Asciidoctor HTML output mess

Asciidoctor is a markup language, similar to markdown. One of its tools, asciidoctor-js (and most probably asciidoctor version written in Ruby), wraps every block (paragraphs, lists and so on) in a div. Resulting HTML is a mess.

For example, simple code listing is wrapped in three divs:

<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-vue" data-lang="vue">&lt;script setup lang='ts'&gt;
import { useFocus } from '@vueuse/core'
&lt;/script&gt;</code></pre>
</div>
</div>
</div>

Emacs: How to add bookmarks to files and directories

Very useful feature of Emacs: You can bookmark directories and files and switch between them quickly.

To add the current dired directory to bookmarks, press M-x r m in dired, then type bookmark name.

To add bookmark to a file, use the same keystroke in a window with a file.

To jump to a bookmark press M-x r l, move to required bookmark in the list and hit enter

Far2l file manager

Far2l is a port of Far Manager v2 that runs on Linux and Mac.

Installation

On Linux distros, first search for far2l package. If it’s not awailable, you have to build it by yourself.

On macs, use the brew package manager:

brew install --cask far2l

Screenshots

Far2l main screenshot

Usage

Interface is pretty the same, but of course far2l is not so much powerful as original Far Manager.

Basic commands for file manipulations are the same as in Far:

  • Tab - jump between left and right panels
  • F5 - Copy file under cursor/selection to the opposite panel
  • F6 - Move file under cursor/selection to the opposite panel
  • Shift-F5 - Create copy of a file file in the same panel
  • Shift-F6 - Rename current file
  • Enter - open file
  • Shift+arrows up/down - select files
  • F3 - View file under cursor
  • F4 - Edit file under cursor

Development state

As of 2024, far2l seems actively developed.

JS: focus on the first input with an error

In this article, I’ll show you how you can easily focus on the first element with an error on your form. The methodology is framework-agnostic, so you can easily adopt it to your framework (if you use any).

TL;DR

Add a data-error attribute to an element if it has an error and then focus on it with querySelector + focus().

Project setup

We’ll use vite + vanilla ts for our project:

npm create vite@latest my-vue-app -- --template vanilla-ts

Now we need some cleanup - remove all content from the styles.css file as well as from the main.ts.

Create a form

First, let’s add markup for our form (main.ts file):

import './style.css'

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div class="personal-form">
  <div class="text-input">
    <label for="name">Name</label>
    <input id="name" type="text">
    <div class="error-label" id="error-name"></div>
  </div>

  <div class="text-input">
    <label for="email">Email</label>
    <input id="email" type="text">
    <div class="error-label" id="error-email"></div>
  </div>

  <div class="text-input">
    <label for="address">Address</label>
    <input id="address" type="text">
    <div class="error-label" id="error-address"></div>
  </div>

  <div class="text-input">
    <label for="passport">Passport</label>
    <input id="passport" type="text">
    <div class="error-label" id="error-passport"></div>
  </div>
  <button id="send"> Send </button>
</div>
`

And this is our css (style.css file):

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
}

.text-input {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.personal-form {
  display: flex;
  flex-direction: column;
  max-width: 70ch;
  margin: 0 auto;
  gap: 1rem;
}

.error-label {
  color: red;
  font-size: 14px;
}

.error-label:empty {
  display: none;
}

The most important thing here is the error-label class - we hide error blocks if they’re empty( with the help of the empty pseudo-class).

Now our page looks this way:

form image

Define data structure

We store each field’s value in its own variable:

let name = ''
let email = ''
let address = ''
let passport = ''

Errors are stored in a single object:

const errors = {
  name: '',
  email: '',
  address: '',
  passport: '',
}

We need to update our variables’ values when text in fields is changed:

function updateValue(inputId, cb: (value: string) => void) {
  document.getElementById(inputId).addEventListener('input', (e: Event) => {
    const value = (e.target as HTMLInputElement).value
    cb(value)
  })
}

This function adds a listener to the input’s input event and we exute it for each of our fields:

updateValue('name', (value) => name = value)
updateValue('email', (value) => email = value)
updateValue('address', (value) => address = value)
updateValue('passport', (value) => passport = value)

When the text in a field is changed, a callback function is executed, which in its turn assigns this value to a corresponding variable.

And finally, code that is responsible for the form’s logic:

function onSubmit() {
  validate()
  displayErrors()
  focusOnError()
}

document.getElementById('send').addEventListener('click', onSubmit)

The idea is simple -

Now we need to implement each from these steps.

Add validation

Our validation logic lives in the validate function:

function validate() {
  // First of all, reset all error messages
  for (const key in errors)
    errors[key] = ''

  if (!name.length)
    errors.name = 'Required field'
  else if (name.length > 30)
    errors.name = '30 characters max'

  if (!passport.length)
    errors.passport = 'Required field'
  else if (passport.length !== 13)
    errors.passport = 'Pasport id should be 13 characters long'
}

It validates only two fields, but that is enough for the demonstration. You can read article about validation with the help of the zod framework here. It is worth noting that this function doesn’t do anything with visual representation on the page - it only validates variables’ values and writes error messages to another variables.

Display errors

After our form’s data is validated, it’s time to display errors if there are any. The algorithm is simple: we iterate over the errors object and update inner text of the error labels with corresponding value. Each error label’s id contains field name (error-nameerror-passporterror-email etc), so we can rich them with document.getElementById function.

Besides that, we are adding a data-error attribute to the input fields which are invalid.

So, here it is:

function displayErrors() {
  for (const key in errors) {
    setErrorValue(key, errors[key])

    if (errors[key])
      document.getElementById(key)?.setAttribute('data-error', errors[key])
    else
      // Don't forget to remove attribute from the input field if its value
      // is correct
      document.getElementById(key)?.removeAttribute('data-error')
  }
}

setErrorValue function searches for an error label and updates its text:

function setErrorValue(field: string, error: string) {
  const el = document.getElementById(`error-${field}`)
  if (el)
    el.innerText = error
}

Set focus on a field with an error

We use document.querySelector to get the first element in the DOM tree with a data-error attribute and set focus on it:

function focusOnError() {
  document.querySelector('[data-error]')?.focus()
}

That’s all! Now, on submit, focus will be set on the first field with an invalid value:

validation result

Full source code

src/main.ts:

import './style.css'

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div class="personal-form">
  <div class="text-input">
    <label for="name">Name</label>
    <input id="name" type="text">
    <div class="error-label" id="error-name"></div>
  </div>

  <div class="text-input">
    <label for="email">Email</label>
    <input id="email" type="text">
    <div class="error-label" id="error-email"></div>
  </div>

  <div class="text-input">
    <label for="address">Address</label>
    <input id="address" type="text">
    <div class="error-label" id="error-address"></div>
  </div>

  <div class="text-input">
    <label for="passport">Passport</label>
    <input id="passport" type="text">
    <div class="error-label" id="error-passport"></div>
  </div>
  <button id="send"> Send </button>
</div>
`

let name = ''
let email = ''
let address = ''
let passport = ''

const errors = {
  name: '',
  email: '',
  address: '',
  passport: '',
}

function updateValue(inputId, cb: (value: string) => void) {
  document.getElementById(inputId).addEventListener('input', (e: Event) => {
    const value = (e.target as HTMLInputElement).value
    cb(value)
  })
}

updateValue('name', (value) => name = value)
updateValue('email', (value) => email = value)
updateValue('address', (value) => address = value)
updateValue('passport', (value) => passport = value)

function onSubmit() {
  validate()
  displayErrors()
  focusOnError()
}

document.getElementById('send').addEventListener('click', onSubmit)

function validate() {
  // First of all, reset all error messages
  for (const key in errors)
    errors[key] = ''

  if (!name.length)
    errors.name = 'Required field'
  else if (name.length > 30)
    errors.name = '30 characters max'

  if (!passport.length)
    errors.passport = 'Required field'
  else if (passport.length !== 13)
    errors.passport = 'Pasport id should be 13 characters long'
}

function setErrorValue(field: string, error: string) {
  const el = document.getElementById(`error-${field}`)
  if (el)
    el.innerText = error
}

function displayErrors() {
  for (const key in errors) {
    setErrorValue(key, errors[key])
    if (errors[key])
      document.getElementById(key)?.setAttribute('data-error', errors[key])
    else
      document.getElementById(key)?.removeAttribute('data-error')
  }
}

function focusOnError() {
  document.querySelector('[data-error]')?.focus()
}

src/style.css:

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
}

.text-input {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.personal-form {
  display: flex;
  flex-direction: column;
  max-width: 70ch;
  margin: 0 auto;
  gap: 1rem;
}

.error-label {
  color: red;
  font-size: 14px;
}

.error-label:empty {
  display: none;
}

Pros and cons

  • Framework-agnostic, can be used with vanilla JS
  • Won’t work if html has non-default order of elements (because querySelector returns the first matched element)

Vue: How to check for a specific parent component

The problem: you want to make sure that a component is placed inside some other component.

The reason why you may want to do this is that your component has no sense when used outside its parent. For example:

<ContextMenu>
  <ContextMenuItem icon="home-icon" text="Home" />
  <ContextMenuItem icon="cut-icon" text="Cut" />
  <ContextMenuItem icon="paste-icon" text="Paste" />
</ContextMenu>

Here, the ContextMenuItem is usable only as a “brick” of the ContextMenu.

In such cases, you can use provide/inject:

  1. Provide a value in ContextMenu
  2. Inject the value in ContextMenuItem. If it’s not defined, throw an error.

Example

As usual, create an empty vite + vue + typescript project:

npm create vite@latest parent-component -- --template vue-tsc

First of all, we need a key for provide/inject functinos, so let’s add it. Create the key.ts file in the root of the project with this content:

import type { InjectionKey } from 'vue'
export const key = Symbol() as InjectionKey<string>

Then, in the components directory, create a ContextMenu.vue file:

<script setup lang="ts">
import { provide, inject } from 'vue'
import { key } from '../key'

provide(key, 'value')
</script>

<template>
  <ul>
    <slot />
  </ul>
</template>

Then, create a ContextMenuItem.vue file:

<script setup lang="ts">
import { inject } from 'vue'
import { key } from '../key'

const val = inject(key)

if (!val)
  throw new Error('ContextMenuItem can be used only inside ContextMenu component!')
</script>

<template>
  <li> Menu Item </li>
</template>

And finally, let’s show our ContextMenu component in the App.vue:

<script setup lang="ts">
import ContextMenu from './components/ContextMenu.vue'
import ContextMenuItem from './components/ContextMenuItem.vue'
</script>

<template>
  <div>
    <ContextMenu>
      <ContextMenuItem />
      <ContextMenuItem />
      <ContextMenuItem />
    </ContextMenu>
  </div>
</template>

Now, on our main page, we’ll see three “Menu item” strings, one per each ContextMenuItem. Open dev console, and make sure you don’t see any errors or warnings.

Everything works as expected - ContextMenuItem is inside ContextMenu. But if we remove ContextMenu component, our dev console will show us an error:

error from the ContextMenuItem component

Should I always check for such situations?

Of course no. Check for the parent component if you really need this. If you’re in doubt, I consider to update your documentation instead of adding more JS code to your project.

References

vue-tsc in watch mode

vue-tsc can be run in a watch mode and report any typescript errors as you change files in a project.

Simply run it with this command:

npx vue-tsc --noEmit --watch

Why you may want to use it? I personally prefer to disable any linter/LSP messages when I need more concentration on a task and I don’t want to be distracted by UI buzz that IDE or editor’s plugins create.