lblod / frontend-embeddable-notule-editor

Frontend for an embeddable variant of the editor.
MIT License
1 stars 1 forks source link

@lblod/embeddable-say-editor

This application allows you to embed the RDFa editor in other applications without integrating with EmberJS directly. It will behave like any other HTML editor.

The readme is structured as follows:

Live Demo

A live demo is available for easy testing. This environment is NOT suited for any production use, as it might change without notice and might be an outdated version. Any content entered here will not be saved.

Using the embeddable editor in your app

With npm

npm install @lblod/embeddable-say-editor

We export a simple function to launch the editor in your app. It currently renders inside an iframe element. A WebComponent version is also in the works and should be available soon after we work out the kinks.

as an iframe

import { renderEditor } from '@lblod/embeddable-say-editor';

// make a container element for the editor to render in in your html
// the id is not required, you just need to be able to get hold of this element 
// in whatever way you like

// note: the editor will replace all children of this element, so best to keep it empty.
// <div id="editorContainer"></div>

const container = document.getElementById('editorContainer');
const editor = await renderEditor({
  element: container,
  title: 'my editor', // optional, this will set the "title" attribute of the iframe
  width: '500px', // width attribute of the iframe
  height: '300px', // height attribute of the iframe
  // optional, if true the editor will grow to fit the content. 
  // When this is true, the height option will determine the minimum height at which the editor starts
  growEditor: true, 
  plugins: [], // array of plugin names (see below)
  options: {} // configuration object (see below)
  })

// the editor is now initialized and can be used
editor.setHtmlContent('hello world');

Using a CDN

If you can't use an npm package directly in your app, the easiest way is using a CDN such as unpkg.com to use the version from npm directly in a <script> tag. For details on how to use start and customise the editor, see the basic code example section below.

Unlike the example which does not specify the version, for production use, we recommend to use a fixed major version number to avoid breaking changes. The changelog can be seen on Github, any update with breaking changes will have a higher version number. For example, to have the latest version of the v3 release, use the following import:

[!NOTE] the version string can be any semver range or tag supported by unpkg.

<script src="https://unpkg.com/@lblod/embeddable-say-editor@^3.2.1"></script>

Using the prebuilt bundles

Previously we suggested using prebuilt versions of the packages hosted on a lblod.info domain. Due to the inability of this system to appropriately support multiple versions, this service is deprecated and will be deactivated in the near future. Migration to the unpkg.com hosted version is easy and will allow you to control which version you want to use.

Simply refer to the basic example below for the basic setup. Instead of passing the plugin configuration to the "initEditor" function, pass it directly to the renderEditor function and await it. The resulting object will be fully initialized and ready for use.

Basic Example: The editor in an HTML file

The idea is that you can have multiple HTML tags in which you can initialize an editor. We'll explain how it works and the process can be repeated if multiple editors are required. We need some HTML structure to start with, and then set-up the editor when everything has finished loading and rendering. We can also initialize the editor with some content or set it later via an event. Use something like the following HTML snippet as a base.

In this section, we will assume you are using unpkg, but using another service that serves npm modules or building and hosting the bundles yourself should work just as well. Simply link the corresponding files to the correct location for your setup.

For an interactive example, refer to this jsfiddle.

<!DOCTYPE html>
<html>

  <head>
    <title>I have an editor in my document</title>
    <script src="https://unpkg.com/@lblod/embeddable-say-editor@^3.2.1"></script>
  </head>

  <body>
    <div id="my-editor"></div>
  </body>

</html>

Next, we'll instantiate the editor. We wait until the DOM has loaded and then render the editor inside it. Put this script in the head of the HTML page with <script>...</script>, or use another method if desired (e.g. at the bottom of the HTML, or in a separate js file you refer to in the html).

window.addEventListener('load', async function() {
  const renderEditor = window['@lblod/embeddable-say-editor'].renderEditor;

  const editorContainer = document.getElementById('my-editor');
  const editorElement = await renderEditor({
    element: editorContainer,
    title: 'my editor', // optional, this will set the "title" attribute of the iframe
    width: '500px', // width attribute of the iframe
    growEditor: true, // optional, if true the editor will grow to fit the content, this will disregard the height attribute
    height: '300px', // height attribute of the iframe
    plugins: arrayOfPluginNames, // array of plugin names (see below)
    options: configurationOptions, // configuration object (see below)
  })
})

See below for everything you can do with the editorElement.

Editor API

The RDFa editor uses the Prosemirror toolkit as a base. After the editorElement.initEditor() function is called and awaited, you will have access to the editor methods, including the controller with editorElement.controller. The renderEditor function does this for you, but you might want to call it again at a later point to re-initialize the editor. This controller is an instance of the SayController class of the ember-rdfa-editor.

editorElement API

These are functions available from the editor element, which is the HTML element with the class notule-editor.

controller API

These methods are accessible via editorElement.controller and contain a way to directly interact with the Prosemirror logic underneath. This is an instance of SayController. Not all possible methods are shown.

Interacting via Prosemirror Commands

A good way to add extra interaction to editor, besides the plugins and buttons provided, is via Prosemirror commands. Most of the plugins also use these commands. A command can be a command from Prosemirror or a custom command. These commands might check certain conditions and run withTransaction to change something in the editor. A good run-through of how they work can be found on the ember-rdfa-editor wiki.
Do note that more advanced commands will need knowledge about the used schema and other internal information of Embeddable. You can access this (via the mainEditorState), but as this is not yet made public, the schema might change in the future.

Configuring The Editor

The editor can be customized to best fit your application.

General Config options

There are some options you can pass to options in renderEditor that are not connected to a plugin.

Plugin system

The Say-editor is conceptualized as a core component which can be extended through a powerful plugin system. This plugin system mirrors Prosemirror's own system, and in fact, the core itself is built using a prosemirror instance with various always-enabled plugins, UI elements, and custom commands and utils.

Currently, due to portability concerns, this system is not directly exposed to the embeddable editor. Instead, embeddable ships with a few pre-defined plugins which can be turned on or off, and have some of their configuration exposed.

However, for using and configuring embeddable, it is still useful to understand some of the concepts that plugins use to create a smart editor.

Managing Plugins

Embeddable ships with the following plugins available. This Readme contains all the important info and configuration for the plugins. For more technical and Ember-specific explanations of every plugin, you can check out the Readme of lblod/ember-rdfa-editor-lblod-plugins.

Every plugin can be enabled by passing its name to the plugins array and optionally its configuration in the options object with the renderEditor helper or in the initialization function initEditor(arrayOfPluginNames, configurationOptions). Any configuration value not provided will use the default value, which are shown in the example configs of the plugins.

Article Structure

This plugin is in charge of inserting and manipulating structures. There are several insertion buttons in the right sidebar under Document Structuren.

After inserting a structure and selecting it, a card will show options to move and delete the structure. These might be disabled if the action is not possible. Using the button en inhoud verwijderen will also delete everything included in the structure, instead of just the closest heading.

Anything part of the block group (almost everything) is allowed under these structures. However, the article structure nodes themselves are only allowed in a specific order.

article structure card and insert buttons example


:heavy_plus_sign: Enable by adding "article-structure" to the plugins array.

These structures are not part of the block group. You will need to edit docContent to accept one of these structures as a base. The following config will allow a title, chapter or article to be added as the first node, or any general block. See general config options for more info.

// pass to options
docContent: '((title|block)+|(chapter|block)+|(article|block)+)'

rdfa-awareness

The structures the plugin inserts are rdfa-annotated according to a custom model. This model is as of yet undocumented. For more information please contact the team.

Besluit

:heavy_plus_sign: Enable by adding "besluit" to the plugins array.

Contextual mode

By default, this plugin scans for the existence of a div with a typeof attribute with a value containing the Besluit type. It also needs a BesluitType, a prov:generated property, and a uri (which should be unique for each besluit). If the selection is inside such a node, the plugin will provide some controls to work with articles inside a besluit.

These articles are always inserted in the value (annotated using the prov namespace) of the besluit, which means a node of that type must also be present. a minimal besluit template which activates all of this plugin's features looks something like this:

<div typeof="http://data.vlaanderen.be/ns/besluit#Besluit https://data.vlaanderen.be/id/concept/BesluitType/4d8f678a-6fa4-4d5f-a2a1-80974e43bf34"
     property="prov:generated"
     resource="http://data.lblod.info/id/besluiten/1">
  <h5>Beslissing</h5>
  <div property="prov:value" datatype="xsd:string">
  </div>
</div>

But in practice a much more elaborate template is typically used, [see here] (https://github.com/lblod/frontend-embeddable-notule-editor/blob/ab5a9619385f4b795a44a675fdc30b658bdcb344/public/test.html#L91) for an example.

Direct mode

Alternatively, you can directly configure the URI of the besluit. This is useful in case you want to use multiple editors to edit the constituent parts of a single decision, in which case you can't provide the required context inside the document.

const options = {
  besluit: {
    decisionUri: 'http://my-endpoint.be/id/besluiten/1234'
  }
}

In this mode, the plugin will not search in the way described above, and will instead allow you to insert articles anywhere in the document, linking them to the provided URI.

BesluitTopic plugin

BesluitTopic plugin is a dependent on besluit plugin. It allows to insert and edit topics of a besluit. By default available topics are fetched from the https://data.vlaanderen.be/sparql endpoint, but this can be configured via the options.

const options = {
  besluitTopic: {
    endpoint: 'https://data.vlaanderen.be/sparql',
  }
}

It is then possible to manage topics from the toolbar. The cursor should be inside a besluit node to see the button.

besluit topics

LPDC plugin

LPDC plugin is a dependent on besluit plugin. It allows to insert LPDC codes.
There is no public endpoint available for LPDC codes, so you will need to provide your own. See here for more information.

Once you have an endpoint, you can configure it like this:

const options = {
  lpdc: {
    endpoint: 'https://some.endpoint.be/lpdc',
  }
}

It is then possible to insert LPDC code nodes in the body of a besluit node. If you aren't able to provide a besluit node, you can instead configure the URI of the decision directly, like so:

const options = {
  lpdc: {
    endpoint: 'https://some.endpoint.be/lpdc',
    decisionUri: 'http://my-domain.be/id/besluiten/1234'
  }
}

lpdc plugin

Citation

Add the possibility to add references to specific legal documents. There are two ways to use this plugin

A. Insert Button Click the button citeeropschrijft toevoegen in the right sidebar. This will open a modal where you can search for different types of legal documents, preview them and insert them if desired.

B. Type Keyword Type one of the trigger phrases, where [words to search for] will be filled in as a search term.

After typing this trigger phrase, a card will appear in the right sidebar with the type and search term filled in. Click Uitgebreid zoeken to pop open the same modal as shown in A.

citation plugin examples


:heavy_plus_sign: Enable by adding "citation" to the plugins array.

// pass to options

// activate everywhere using type 'ranges'
citation: {
  endpoint: '/codex/sparql',
  type: 'ranges',
  // The doc node is the main node and contains the whole document
  activeInRanges: (state) => [[0, state.doc.content.size]],
},

// activate everywhere using type 'nodes'
citation: {
  endpoint: '/codex/sparql',
  type: 'nodes',
  activeInNodeTypes(schema, _state) {
    // the root node of the document is the doc.
    return new Set([schema.nodes.doc]);
  }
},

Both examples show how to activate the plugin for the whole document, which is also the default.

rdfa-awareness

The citations inserted are rdfa-annotated, but as you can see above, this plugin uses a different mechanism to determine where it is active.

Roadsign Regulation

Add annnotated mobiliteitsmaatregelen from a specified registry, which will most likely be using the public facing sparql endpoint of the roadsign registry. This data is maintained by experts at MOW Vlaanderen.


:heavy_plus_sign: Enable by adding "roadsign-regulation" to the plugins array.


// pass to options
roadsignRegulation: {
  endpoint: 'https://dev.roadsigns.lblod.info/sparql',
  imageBaseUrl: 'https://register.mobiliteit.vlaanderen.be/',
  // optional
  decisionUri: 'http://my-endpoint.be/id/besluiten/1234',
  // optional
  // see below for valid decisiontypes in which the plugin will activate
  decisionType:'https://data.vlaanderen.be/id/concept/BesluitType/4d8f678a-6fa4-4d5f-a2a1-80974e43bf34'
}

rdfa-awareness

:warning: Unless you pass the above decisionUri and decisionType options, this plugin will only activate in besluiten with a certain rdf type. You will also need to activate the Besluit plugin to be able to create besluiten.

Exhaustive list of decision types in which this plugin will activate https://data.vlaanderen.be/id/concept/BesluitType/4d8f678a-6fa4-4d5f-a2a1-80974e43bf34 https://data.vlaanderen.be/id/concept/BesluitType/7d95fd2e-3cc9-4a4c-a58e-0fbc408c2f9b https://data.vlaanderen.be/id/concept/BesluitType/3bba9f10-faff-49a6-acaa-85af7f2199a3 https://data.vlaanderen.be/id/concept/BesluitType/0d1278af-b69e-4152-a418-ec5cfd1c7d0b https://data.vlaanderen.be/id/concept/BesluitType/e8afe7c5-9640-4db8-8f74-3f023bec3241 https://data.vlaanderen.be/id/concept/BesluitType/256bd04a-b74b-4f2a-8f5d-14dda4765af9 https://data.vlaanderen.be/id/concept/BesluitType/67378dd0-5413-474b-8996-d992ef81637a

When the cursor is inside such a besluit, the button Voeg mobiliteitsmaatregel in will appear under the insert menu. Clicking this will show a modal to filter and select roadsign regulation to insert.

roadsign regulation modal

Table of Contents

Add a table of contents at the top of the document. It can be toggled with a button in the top toolbar.

At this time it will only work well together with article-structure plugin by using the default config.

:warning: For use in different situations, open an issue on this repo with the usecase, so we can help. The Prosemirror schema that is used in the config is public-facing yet, so changing this is not trivial.


:heavy_plus_sign: Enable by adding "table-of-contents" to the plugins array.

// pass to options
docContent: /*adjust to include `table_of_contents?` as an accepted node*/ ,
tableOfContents: [
  {
    nodeHierarchy: [
      'title|chapter|section|subsection|article',
      'structure_header|article_header',
    ],
  },
],

note: this config is a list. Multiple nodeHierarchys can be passed to let the table of contents work in multiple situation. The last matching hierarchy will be used.

rdfa awareness

As mentioned above, this plugin will only work with structures from the article-structure plugin, which are of course rdfa-annotated.

RDFa Variables

These are placeholders that can be inserted in a document. A variable placeholder has a specific type (text, number, date, address or codelist), which changes the type of input it can receive. These placeholders can then be filled in by a user using the document.

Usually variables are inserted in an editor made to create templates (documents to be filled in), and only edited in an editor to fill in these templates. Via the config you can customize if you want to allow insertion and/or filling in a variable.
Note: a user will always be able to remove a variable, even if insertion is not allowed.

A variable can be inserted with the card shown in the right sidebar.
insert variable card

Types of variables:

rdfa-awareness

The serialization format of these variables uses rdfa to store its data.

Formatting Toggle

This will add a button Toon opmaakmarkeringen in the top toolbar. This toggles the visibility of all formatting marks of the document such as break lines, paragraph markers and others.
document with formatting annotations


:heavy_plus_sign: Enable by adding "formatting-toggle" to the plugins array. No configuration is needed.

rdfa-awareness

none.

Rdfa Blocks Toggle

This will add a button Toon annotaties in the top toolbar. This toggles the visibility of RDFa information contained in the document. This is useful if you want to check for errors in the RDFa structure, or simply have a look at what data the editor is generating behind the scenes. As such, it is mostly useful for expert users.

document with rdfa blocks visible


:heavy_plus_sign: Enable by adding "rdfa-blocks-toggle" to the plugins array. No configuration is needed.

rdfa-awareness

Visualizes rdfa.

(Experimental) RDFA edit and debug tools plugin

Enables (experimental) RDFA edit and debug tools plugin. This plugin is visible in the side panel.
Please see here for more information.

img.png

Template Comments

Adds buttons to the right sidebar for insertion, moving and removing of comment blocks, also called toelichtings- of voorbeeldbepaling. These blocks are meant to provide extra info to users filling in a document that do not need to be published when the document is complete.

It has a special RDFa type ext:TemplateComment with ext the prefix for http://mu.semte.ch/vocabularies/ext/, so this can be filtered out when a document is finished.


:heavy_plus_sign: Enable by adding "template-comments" to the plugins array. No configuration is needed.

rdfa-awareness

The serialization format of the comment blocks uses rdfa.

Confidential Content

Adds a toolbar button to redact content. This simply adds an RDFa annotation with particular styling applied to it. It is up to any processor handling the document to properly redact the content.


:heavy_plus_sign: Enable by adding "confidentiality" to the plugins array. No configuration is needed.

rdfa-awareness

Adds the RDFa annotation property set to ext:redacted for the redacted text.

HTML Edit & HTML Preview

Adds toolbar buttons to interact with the HTML produced by the editor. Either plugin can be used independently or they can be used together.

"html-edit" enables a modal which displays the HTML representation of the current editor contents and allows them to be modified.

"html-preview" enables a modal which renders a preview of the editor contents if they were exported as HTML and put into an otherwise blank HTML document.


:heavy_plus_sign: Enable the HTML Editor by adding "html-edit" to the plugins array. Enable the HTML Export Preview by adding "html-preview" to the plugins array. No configuration is needed.

rdfa-awareness

While there are no specific RDFa aware features of these plugins, when the RDFa Aware mode of the editor is activated, this will significantly influence the markup that the editor produces, as it will now contain hidden elements to contain the annotations. This also limits the HTML that can be entered as it will be processed so that it continues to be valid before it is applied to the editor.

Location

Adds a button in the right sidebar to allow the user to insert a location in flanders.

Configuration

:warning: Unlike most plugins, the default configuration is not production ready and has to be adjusted. :warning:

The only configuration this plugin needs is the base URI for annotating the locations. Unfortunately we cannot provide a reasonable default for this, because it is up to the application to manage its URI namespace. You can also give the default value for the 'municipality' search field.

If you are unsure which base to choose here, we might be able to help you figure it out.

{
  location: {
    defaultPointUriRoot: 'https://example.net/id/geometrie/',
    defaultPlaceUriRoot: 'https://example.net/id/plaats/',
    defaultAddressUriRoot: 'https://example.net/id/adres/',
    defaultMunicipality: 'Gent',
  }
}

:heavy_plus_sign: Enable the Location plugin by adding "location" to the plugins array.

Usage

There are currently 3 ways to define a location:

Address

This is the default mode. Address lookups target the flemish location services

Show image ![img.png](docs/location-plugin-address-mode.png)

When searching for the address, the map will update and show the selected location:

Show image ![img.png](docs/location-plugin-address-mode-filled.png)
Point location

This is the second mode, accessible by selecting the corresponding tab in the modal. It is meant for referring to locations which don't really have a sensible address, such as playgrounds, parks, statues, etc.

The user is required to provide a name for the location, which will appear in the text. The location will be annotated with its geographical coordinates.

Show image ![img.png](docs/location-plugin-point-mode.png)

In this mode, the search feature only centers the map. The user can then click on the map to choose a specific location.

Area location

In this mode, the user can draw an arbitrary shape on the map. This can be used to specify zones such as neighbourhoods, hospital campusses, or even entire municipalities. The given name for the area will be inserted into the text, which will be annotated with the coordinates of the shape's points.

Show image ![img.png](docs/location-plugin-area-mode.png)

Click on the map to create a shape. Each subsequent point will connect in a straight line to the previous point. To complete the shape, click on the first point again. To delete the last point you added, click on it again. To change an existing shape, simply start drawing a new one. When it is completed, it will replace the old shape.

Enabling/disabling the environment banner

The environment banner is a visual indication of the environment you are currently using and which versions of Embeddable, the editor and editor-plugins are in use.

You can enable/disable the banner using the following methods: enableEnvironmentBanner and disableEnvironmentBanner.

Localization

Localization of the editor is an ongoing effort, the main target usage of Embeddable is currently Dutch speaking users. The editor will use the user's browser language and supports English (en-US) and Dutch (nl-BE). If the user has a different language set, the editor will default to Dutch.

Some plugins, like the citation plugin, use date pickers. The display format of these dates are connected with the local.

The locale can be overwritten with setLocaleToDutch(), setLocaleToEnglish() and setLocale(locale: string). You can call one of these functions after initEditor() to always use the same language for the editor, ignoring the user's browser language.

Styling

Styling the editor is supported via setting the CSS Variables used by the editor. You can do it in two ways:

Setting CSS variables at runtime

When using the npm package

When initializing editor with renderEditor pass an object cssVariables with the variables you want to set.

const cssVariables = {
  '--say-font-family': 'Comic Sans MS', 
}

const editor = await renderEditor({
  // all other config options
  cssVariables,
})

When using the prebuilt bundles

After calling the initEditor on the editorElement you can use the snippet to modify CSS variables used by the editor

// initialization code
editorElement.initEditor(/* some config */);

// Example of calling the setProperty method on the editorElement
// Will set default font used by editor to Comic Sans MS
editorElement.style.setProperty('--say-font-family', 'Comic Sans MS');

Setting CSS variables by targeting the editor in your own CSS

[!IMPORTANT] This wont work if you are using the npm package and the renderEditor it provides, as the editor is isolated inside an iframe.

This method uses the CSS specificity to override the default CSS variables used by the editor. You can target the editor by using the notule-editor class on the element that contains the editor.

Below example expects that the editor was attached to an element with id my-editor.

#my-editor .notule-editor {
  --say-font-family: 'Comic Sans MS';
}

Exposed CSS variables

Important Concepts

RDFA

The rdfa standard is a way to add data annotations to xml (and in particular, html) documents. It uses linked data as its data modelling method. RDFA-annotated html is the one and only document format of the say-editor. Because it is a strict superset of html, this also means that the editor can be used as a plain WYSIWYG html editor. But the addition of rdfa-aware tools and features is the editor's unique strength, and the reason for its existence.

Throughout the editor and its plugins, the rdfa-annotated html document is the single format which contains all information. This means that any document metadata is also stored in this standard way, allowing easy interop with other linked-data tools.

In fact, it can be interesting to paste the output of getHtmlContent() in the reference rdfa parser to see what data the parser can extract from the document.

(Note: it's important to use the getHtmlContent() method as opposed to copying the html from the browser inspector. We do not guarantee compliance with the standard in the live, editable, html.)

RDFA-aware plugins

Most plugins use RDFA in some way to provide their features.

In some cases, they simply use it as a way to store information they need to operate. For example: the variable plugin will insert nodes in the document that are rdfa-annotated with certain properties that the plugin interacts with. When loading a document from html, this is what the plugin will use to determine whether to render its special interactive "pills" for a particular node.

In other cases, plugins use rdfa to determine whether they should be "active" (show their UI) or not. This is usually done based on the idea of "context".

Because html, and also the internal prosemirror datastructure, is a tree, there is an inherent hierarchy to the document. At the top there's a root element, usually a div, which we also call the doc node, which contains the entire document. It also contains the selection, the blinking text cursor or blue region that you are surely familiar with. This idea of the selection being "inside" a certain node is what drives the context-aware plugins. Essentially, all they do is walk up the tree structure from the point of the cursor, and see if they encounter any nodes they're interested in.

This means we can have different plugins active depending on where you are in the document!

A third way a plugin might use rdfa, is by searching the document for the existence of a particular rdfa-annotated node, and interacting with it (by adding content in that node, for example).

Technical note: rather than interacting with html/rdfa directly, plugins interact with prosemirror's internal datastructure. This is why adjusting the page html in the inspector or with javascript will not give any meaningful results. The provided interfaces are the only supported ways to interact with the editor. In fact, the plugins each get their own controller, which is identical to the controller we expose on the embeddable element.

Development of @lblod/embeddable-say-editor

Prerequisites

You will need the following things properly installed on your computer.

Installation

Running

Building

Developing

How it works

This repository includes the editor and editor plugins packaged together with Ember so the editor can be used in projects outside of Ember. This is mostly done by adjust the build process in ember-cli-build.js, by specifying the output filename and limiting the chunks to one file, for easy importing.

the consumer loads the editor in their own div element. This editor is fully defined in app/components/simple-editor.js, with consumer-facing logic bound in insertedInDom and logic that needs a controller bound in handleRdfaEditorInit. Because the editor is a black box for the consumer, it is not possible to load plugins the same way as in Ember for them. Instead, all plugins are loaded in ember code depending on a config the consumer passes.

The consumer will access the controller and other methods by accessing the notule-editor element. For developing, you can access this element easily in the console by searching for the div with class="notule-editor", right-click and select click "use in console" (Firefox) or "store as global variable" (Chrome).

Important Develop notes

Specific Embeddable Quirks

Further Reading / Useful Links