1.0.18 β€’ Published 3 months ago

otter-editor v1.0.18

Weekly downloads
603
License
MIT
Repository
github
Last release
3 months ago

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} />
PropertyValueRequiredDefault
blocksarray of blocksYesDefines the blocks available in the editor.
initial_dataLoaded document dataThe loaded page data.
block_numbersboolfalseLabel each block with its 1-based index
add_block_msgstring'Insert block'Label for the 'insert block' button
can_add_and_remove_blocksbooltrueIf set to false, the user cannot add or remove blocks. Useful if you want an editor with a single pre-programmed block.
custom_classesObjectAllows you to specify custom CSS classes on a variety of editor elements. See CSS for details.
savefunction(data)Save the document.
update_heightfunction(new_height_in_pixels)Called by Otter when the editor height changes, in case this is useful to you.
open_media_libraryfunction(set_value)Called by Otter when a MediaPicker button is clicked. Call set_value to set the picked item.
dev_modeboolfalseAdd 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

PropertyValueRequiredDefault
type<string>YesThe 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.
fieldsArray(<Field>)YesThe 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>falseIf 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.
tabsArray(<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 pickerPopup (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:

PropertyValueRequired
name<string>YesKey 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.
typeOtter.Field.<FieldType>YesThe 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 appropriateA 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 typeValue
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.

TypeDescriptionOptionsDefault
TextInputPlain text input
TextAreaTextarea (multi-line plain text)
mono (bool)falseUse a monospace font
TextEditorRich text editor
heading_levels (array)[1, 2]Heading types to display in the paragraph style dropdown
bullets (bool)trueEnable bullets
blockquote (bool)falseEnable blockquote
hr (bool)falseEnable horizontal rule
paste_as_plain_textfalseClear text formatting on paste
NumberInputNumber input with min, max, step etc
BoolA toggle
no_label (string)"Yes"Label for true option
yes_label (string)"No"Label for false option
RadiosRadio 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, ...}
ColorSwatchRadiosColor swatch radio buttons
palette (array)In the form [{ label: 'Row', colors: [ {value: 'text-white', output_class_or_hex: 'bg-white'} ]}].
SelectSelect dropdown
options (object)Select options. Key pairs are in the form value: "Label".
NestedBlockEmbed 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)falseIf true, render a toggle that enables/disables the Nested Block
seamless (bool)falseIf 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.
RepeaterEmbed 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 limitOptionally limit the number of items the user can add.
item_headers (bool)falseIf 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.
SearchableText 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)500Adjust search callback rate limiting.
MediaPickerSelect 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.

1.0.18

3 months ago

1.0.17

1 year ago

1.0.16

1 year ago

1.0.15-beta.0

1 year ago

1.0.15

1 year ago

1.0.14

1 year ago

1.0.13

1 year ago

1.0.11

2 years ago

1.0.12

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.10

2 years ago

0.0.169

2 years ago

0.0.164

2 years ago

0.0.168

2 years ago

0.0.167

2 years ago

0.0.166

2 years ago

0.0.165

2 years ago

0.0.172

2 years ago

0.0.171

2 years ago

0.0.170

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

0.0.163

3 years ago

0.0.162

3 years ago

0.0.159

3 years ago

0.0.158

3 years ago

0.0.151

3 years ago

0.0.150

3 years ago

0.0.157

3 years ago

0.0.156

3 years ago

0.0.154

3 years ago

0.0.161

3 years ago

0.0.160

3 years ago

0.0.148

3 years ago

0.0.147

3 years ago

0.0.142

3 years ago

0.0.141

3 years ago

0.0.146

3 years ago

0.0.145

3 years ago

0.0.128

3 years ago

0.0.127

3 years ago

0.0.126

3 years ago

0.0.129

3 years ago

0.0.139

3 years ago

0.0.138

3 years ago

0.0.137

3 years ago

0.0.136

3 years ago

0.0.130

3 years ago

0.0.135

3 years ago

0.0.134

3 years ago

0.0.132

3 years ago

0.0.140

3 years ago

0.0.125

3 years ago

0.0.120

3 years ago

0.0.124

3 years ago

0.0.123

3 years ago

0.0.122

3 years ago

0.0.121

3 years ago

0.0.119

3 years ago

0.0.117

3 years ago

0.0.116

3 years ago

0.0.115

3 years ago

0.0.118

3 years ago

0.0.113

3 years ago

0.0.112

3 years ago

0.0.111

3 years ago

0.0.110

3 years ago

0.0.106

3 years ago

0.0.109

3 years ago

0.0.108

3 years ago

0.0.107

3 years ago

0.0.105

3 years ago

0.0.104

3 years ago

0.0.103

3 years ago

0.0.102

3 years ago

0.0.101

3 years ago

0.0.96

3 years ago

0.0.97

3 years ago

0.0.99

3 years ago

0.0.91

3 years ago

0.0.92

3 years ago

0.0.100

3 years ago

0.0.93

3 years ago

0.0.90

3 years ago

0.0.89

3 years ago

0.0.87

3 years ago

0.0.88

3 years ago

0.0.85

3 years ago

0.0.86

3 years ago

0.0.84

3 years ago

0.0.81

3 years ago

0.0.82

3 years ago

0.0.83

3 years ago

0.0.80

3 years ago

0.0.77

3 years ago

0.0.78

3 years ago

0.0.79

3 years ago

0.0.76

3 years ago

0.0.73

3 years ago

0.0.74

3 years ago

0.0.75

3 years ago

0.0.72

3 years ago

0.0.71

3 years ago

0.0.70

3 years ago

0.0.69

3 years ago

0.0.68

3 years ago

0.0.67

3 years ago

0.0.66

3 years ago

0.0.65

3 years ago

0.0.63

3 years ago

0.0.64

3 years ago

0.0.62

3 years ago

0.0.60

3 years ago

0.0.61

3 years ago

0.0.59

4 years ago

0.0.58

4 years ago

0.0.54

4 years ago

0.0.55

4 years ago

0.0.56

4 years ago

0.0.57

4 years ago

0.0.53

4 years ago

0.0.51

4 years ago

0.0.52

4 years ago

0.0.50

4 years ago

0.0.49

4 years ago

0.0.48

4 years ago

0.0.47

4 years ago

0.0.46

4 years ago

0.0.45

4 years ago

0.0.44

4 years ago

0.0.43

4 years ago

0.0.42

4 years ago

0.0.41

4 years ago

0.0.40

4 years ago

0.0.39

4 years ago

0.0.38

4 years ago

0.0.37

4 years ago

0.0.33

4 years ago

0.0.34

4 years ago

0.0.35

4 years ago

0.0.36

4 years ago

0.0.32

4 years ago

0.0.30

4 years ago

0.0.31

4 years ago

0.0.27

4 years ago

0.0.28

4 years ago

0.0.29

4 years ago

0.0.26

4 years ago

0.0.25

4 years ago

0.0.23

4 years ago

0.0.24

4 years ago

0.0.21

4 years ago

0.0.22

4 years ago

0.0.20

4 years ago

0.0.19

4 years ago

0.0.18

4 years ago