npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

otter-editor

v1.0.18

Published

Content editor with declaratively defined content blocks.

Downloads

186

Readme

Otter, an embeddable content editor

Build Status

Otter is perhaps the ~~furriest~~ easiest way in the universe to embed a content editor in your react/preact application.

An Otter editor 👆

  • Create a full-blown content editor by simply ~~splashing about in a river~~ defining some content models 🏔
  • Simple and ~~estuarine~~ declarative block-’n-field syntax for your models 🌿
  • Generates post data in an ~adorable~ accessible JSON format 💧
  • Delivered as a React component that’s really ~~into fish~~ easy to use 🐟
🐟 🐟 🐟
npm i -S otter-editor --legacy-peer-deps
🐟 🐟 🐟
<Otter.Editor blocks={blocks}
              initial_data={data} />
🐟 🐟 🐟

Contents

<Otter.Editor>

The <Otter.Editor /> element renders the editor.

<Otter.Editor blocks={blocks}
              initial_data={data} />

| Property | Value | Required | Default | | | :--------------- | :--------------------------------------------- | :------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------- | | blocks | array of blocks | Yes | | Defines the blocks available in the editor. | | initial_data | Loaded document data | | | The loaded page data. | | block_numbers | bool | | false | Label each block with its 1-based index | | add_block_msg | string | | 'Insert block' | Label for the 'insert block' button | | can_add_and_remove_blocks | bool | | true | If set to false, the user cannot add or remove blocks. Useful if you want an editor with a single pre-programmed block. | | custom_classes | Object | | | Allows you to specify custom CSS classes on a variety of editor elements. See CSS for details. | | save | function(data) | | | Save the document. | | update_height | function(new_height_in_pixels) | | | Called by Otter when the editor height changes, in case this is useful to you. | | open_media_library | function(set_value) | | | Called by Otter when a MediaPicker button is clicked. Call set_value to set the picked item. | | dev_mode | bool | | false | Add a button to copy a block's data to your clipboard. This lets you easily obtain a block's initial_data. |

Blocks

The Otter editor is based on content models that you define. These block definitions are succinct and declarative. Within a block, there will be one or more Fields. This system lets you rapidly arrange fields into blocks to create rich content editors.

An example Block of type PageHeader might contain the Fields: title, subtitle, and background_image.

{
  type: 'MyBlock',
  description: 'My block',
  fields: [
    <Field>,
    <Field>,
    ...
  ],
}

Block properties

| Property | Value | Required | Default | | | :------------ | :----------------- | :------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | | type | <string> | Yes | | The block type identifier. Each block's type string must be unique within the editor. | | description | <string> | | | A human-readable name for the block, identifying it clearly to the user. If not present Otter will use a prettified version of the block type. | | fields | Array(<Field>) | Yes | | The fields in this block. | | initial_data| <object> | | | Optionally, provide initial data for the block. | | thumbnail | <path> | | | Optional thumbnail for use in the graphical block picker. | | hidden | <bool> | | false | If true, don't display this block in the block picker. This allows you to define blocks at the top level which can only be used in a NestedBlock or Repeater. | | tabs | Array(<Tab>) | | | Allows you to arrange the block's fields into tabs. See block tabs.

Block picker

When supplying your content blocks to Otter, (<Otter.Editor blocks={blocks} />), you can either use a simple, flat array of blocks, or group them into categories.

// Simple blocks
const blocks = [
  <Block>,
  <Block>,
  ...
]
// Grouped blocks
const blocks = {
  text: {
    name: 'Text blocks',
    blocks: [ <Block>, <Block>, ... ],
  },
  media: {
    name: 'Media blocks',
    blocks: [ <Block>, <Block>, ... ],
  },
}
  • If simple blocks are provided, otter renders a popover-style block picker.
  • If grouped blocks are provided, otter renders a popup-style, graphical block picker. This can provide a better user experience:

| Popover (simple) block picker | Popup (grouped) block picker | | :----------------------------------- | :------------------------------------ | | | |

Block tabs

Within a block, fields may be grouped into tabs. This can help you keep the user experience clean when your blocks are more complex:

// Create tabs:
// - 'Text' containing heading and subheading fields
// - 'Settings' containing align field
export const header_block = {
  type:      'Header',
  fields:    [
    { name: 'heading', ... },
    { name: 'subheading', ... },
    { name: 'align', ... },
  ]
  tabs: [
    {
      label: 'Text',
      fields: ['heading', 'subheading'],
    },
    {
      label:   'Settings',
      fields: ['align'],
    },
  ],
}

You can optionally use an icon instead of text to label your tab. When using icon tab labels, the tab icons are placed inside the header of the Block or Repeater item, next to the delete button:

export const header_block = {
  ...
  tabs: [
    {
      Icon: PencilSolid,
      fields: ['heading', 'subheading'],
    },
    {
      Icon:   Cog8ToothSolid,
      fields: ['align'],
    },
  ],
}

Fields

Each block should contain at least one field.

{
  name: 'content',
  description: 'Content',
  type: Otter.FieldTypes.TextArea,
}

Field properties

Properties on fields:

| Property | Value | Required | | | :----------------- | :-------------------------------------- | :--------- | :------------------------------------------------------------------------------------------------------- | | name | <string> | Yes | Key to save the field data under within the block. | | description | <string> | | Field label displayed to the user. If not present Otter will use a prettified version of the field name. | | type | Otter.Field.<FieldType> | Yes | The field type. | | display_if | <DisplayRule>, Array(<DisplayRule>) | | Show/hide this field based on the value(s) of its sibling(s). | | default_value* | Any type, as appropriate | | A default value, used to set the field initially and to provide data on save if the field is empty. | | placeholder | <string> | | For text and textarea inputs. | | class_wrapper** | <string> | | Add custom classes to the field wrapper. See custom layout | | class_label** | <string> | | Add custom classes to the field label. See custom layout | | class_field** | <string> | | Add custom classes to the input or other field element. See custom layout | | mini | <bool> | | Currently only supported on Select, TextInput and Number fields. Useful in combination with Radios etc. |

  • *(default_value is supported on all fields except: TextEditor, MediaPicker, NestedBlock, Repeater, Searchable.)
  • **(class_wrapper, class_label & class_field are supported on all fields except: Repeater, NestedBlock. See custom layout.)

type should be specified using the Otter-defined constants: type: Otter.FieldTypes.TextInput, etc. (See field types.)

With display_if you can show or hide the field based on the value of one or more of its siblings. Each DisplayRule specifies the name of the sibling and a value. You can test against more than one sibling field using an array of multiple DisplayRule objects.

// display_if example:
{
  name: 'url',
  description: 'URL',
  type: Otter.FieldTypes.TextInput,
  display_if: {
    sibling: 'is_link',
    equal_to: true,
  },
}

Beside equal_to, DisplayRule supports these rule types:

| Rule type | Value | | | :------------- | :------------------------------- | -------------------------------------------------------------------------- | | equal_to | <value> | Show the field if the sibling's value === <value> | | not_equal_to | <value> | ...if !== <value> | | matches | <string> (compiled to RegExp) | Show the field if the sibling's value is a string which matches the regex | | doesnt_match | <string> (compiled to RegExp) | ...which doesn't match the regex |

Note that using matches and doesnt_match may impact the performance of typing into the targeted sibling field, as it causes relayout to occur on input.

Field types:

The supported field types and their options are documented below.

| Type | Description | Options | Default | | | :------------------ | :------------------------------------------ | :-------------------------------------------------------| :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | TextInput | Plain text input | | | | | TextArea | Textarea (multi-line plain text) | | | | | | | mono (bool) | false | Use a monospace font | | TextEditor | Rich text editor | | | | | | | heading_levels (array) | [1, 2] | Heading types to display in the paragraph style dropdown | | | | bullets (bool) | true | Enable bullets | | | | blockquote (bool) | false | Enable blockquote | | | | hr (bool) | false | Enable horizontal rule | | | | paste_as_plain_text | false | Clear text formatting on paste | | NumberInput | Number input with min, max, step etc | | | | | Bool | A toggle | | | | | | | no_label (string) | "Yes" | Label for true option | | | | yes_label (string) | "No" | Label for false option | | Radios | Radio buttons | | | | | | | options (object) | | Radio options. Key pairs are in the form value: "Label". | | | | icons (object) | | Use icons to label the radios instead of labels. Format: {opt_value: IconComponent, ...} | | ColorSwatchRadios | Color swatch radio buttons | | | | | | | palette (array) | | In the form [{ label: 'Row', colors: [ {value: 'text-white', output_class_or_hex: 'bg-white'} ]}]. | | Select | Select dropdown | | | | | | | options (object) | | Select options. Key pairs are in the form value: "Label". | | NestedBlock | Embed another block into this block | | | | | | | nested_block (string or Block object) | | The block to embed inside this block. May be either a Block object or the string name of a block defined elsewhere in the blockset. | | | | optional (bool) | false | If true, render a toggle that enables/disables the Nested Block | | | | seamless (bool) | false | If false, a subtle border + padding surrounds the blocks contents, which can aid grouping (visually). If true there is no visual indication that a nested block has been used. | | Repeater | Embed an array of blocks within this block | | | | | | | nested_blocks (array: strings or Block objects) | | The blocks available in this Repeater. Value is an array of either Block objects or name strings of blocks defined elsewhere in the blockset. | | | | max (number) | No limit | Optionally limit the number of items the user can add. | | | | item_headers (bool) | false | If true, repeater items are rendered with a header displaying the name/description of the repeater field. Note: 'with_header' is required to use icon tabs within repeater items. | | Searchable | Text input with custom search | | | | | | | search (function -> promise) | | A function that takes a search term as its argument, performs a search, and returns a promise. The promise should resolve to an array of search results in the form {value, display}. If the promise rejects, it should return an error message string which will be displayed to the user below the search field. | | | | debounce_ms (number) | 500 | Adjust search callback rate limiting. | | MediaPicker | Select item from media library | | | (Note: you must implement a media library, Otter does not include one.) | | | | label (string) | "Select" | Button label. |

Testing for optional repeaters and nested blocks

When an optional block is saved, the data item has an extra boolean property, __enabled. When outputting the document, templates can check this property to see if the block should be rendered or ignored:

header.image.__enabled && <img src="header.image.src" alt="header.image.alt" />

Custom layout

By default, fields take up the full width of their container, with the field label above the field. You can customize this layout by passing classes to these field definition options: class_wrapper, class_label, class_field.

Fields are rendered in a flex container, which gives you a lot of power to customize how fields are laid out in a block.

For example, setting class_wrapper='w-1/2' would allow you to align two fields side by side horizontally. Similarly, you could use class_wrapper and class_label to place the label next to the field instead of above it.

Note that you can easily break the layout using these options. You should take particular care over how layout behaves when the viewport width changes.

For custom layout examples, see the demo.

Demo

The demo project in /demo renders a complete Otter editor, and demonsrates NestedBlock fields, Repeaters, and customisation options.

npm run demo
  # or: parcel demo/index.html

CSS and Tailwind

Otter uses Tailwind (3.0) for styling. Ideally, your application should compile Tailwind.

When using your own compiled Tailwind, your bundle must also must import Otter’s small amount of its own CSS and that of the Quill editor:

import 'otter/dist/css/quill.snow.css'
import 'otter/dist/css/otter.css'
``

If you are not compiling tailwind yourself, you can instead import everything (including a compiled copy of tailwind) from otter/dist:

```js
import 'otter/dist/css/all.css

(Note that while the all-in-one-go all.css method may be a quick way to get started, your project should ultimately take on the tailwind compilation instead of relying on Otter to do so.)

Custom CSS classes

You can customise the look and feel of the editor by passing a nested object of CSS classes to the <Editor /> custom_classes prop.

const custom_classes = {
  typography: {
    heading: 'font-bold tracking-tight',
  }
}
<Otter.Editor custom_classes={custom_classes} ... />

The demo includes a substantial example use of custom CSS classes, see /demo/custom-classes.js.

For all support custom classes, see /src/core/definitions/classes.js.

Selective importing

Sometimes one of your application bundles may not need to import the whole of the Otter library. You can import the field type definitions separately:

import FieldTypes from 'otter-editor/dist/core/definitions/field-types'

And you can also import individual otter utils:

import {set_dynamic_data} from 'otter-editor/dist/core/definitions/utils'

Tests

npm run t   # run all tests
npm run tw  # run all tests, --watch
npm run ts src/test/<file>  # run a single test, --watch

Local development within another project

  • Delete your-app/node_modules/otter-editor
  • From otter, run a build to generate CSS and initial JS:
DIST=/path/to/my-app/node_modules/otter-editor/dist LOCALDEV=yes bash scripts/build.sh
  • If desired, then begin live-compiling from otter into DIST:
DIST=/path/to/my-app/node_modules/otter-editor/dist LOCALDEV=yes WATCH=y bash scripts/babel.sh

License

To enable Wordpress integration, Otter is dual-licensed. The license is:

  • GPLv2 for the purpose of embedding within Wordpress themes
  • MIT for all other purposes

See LICENSE.md.