# WriteAway

WriteAway is a JavaScript library for editing CMS pieces inline on pages on the client side.

Based on React and Redux

Created by SpiralScout.

WriteAway comes like a core controller with set of plugins for different types of media.

@writeaway/core - Core controller for all plugins, includes editor floating bar and manages state of editors

@writeaway/plugin-medium - Plugin for supporting rich text editing, editing images and block backgrounds

@writeaway/plugin-coremirror - Plugin for editing block's source code, i.e. for embedded iframes or scrips

@writeaway/plugin-seo - Plugin for editing SEO meta tags

## Installation On Top Of Existing Project

To use on top of existing project you would need to create a custom bundle using your favorite packaging tool. You can see 2 samples of such bundles on demo website and in spiral framework bridge

This bundle will scan an existing page for specific selectors and will attach editors on them

    npm install --save @writeaway/core
    npm install --save @writeaway/plugin-medium
    npm install --save @writeaway/plugin-codemirror
    npm install --save @writeaway/plugin-seo

And then in code of bundle

        import { WriteAwayCore } from '@writeaway/core';
        import { WriteAwaySeo } from '@writeaway/plugin-seo';
        import { WriteAwayCodeMirror } from '@writeaway/plugin-codemirror';
        import { WriteAwayBackground, WriteAwayImageTag, WriteAwayMedium } from '@writeaway/plugin-medium';

        // Define which piece type is handled by which editor
        const components = {
          html: WriteAwayMedium,
          image: WriteAwayImageTag,
          background: WriteAwayBackground,
          source: WriteAwayCodeMirror,
          seo: WriteAwaySeo,
        };

        const writeaway = new WriteAwayCore({
          piecesOptions: {
            selector: '.js-piece', // Selector to look for
            attribute: 'data-type', // Attribute containing piece type, that will define what editor to attach
            attributeId: 'data-id', // Attribute containing unique piece id
            attributeName: 'data-name', // Attribute containing human readable piece name that will be shown as header in hover block
            components,
          },
        });

Attach resulting bundle in bottom of the page

For styling you can either write own styles or include default to bundle `.less` file like so

    @import "~@writeaway/core/src/styles/writeaway";
    @import "~@writeaway/plugin-medium/src/medium-editor";
    @import "~@writeaway/plugin-medium/src/writeaway-medium";
    @import "~@writeaway/plugin-seo/src/google-preview";
    @import "~@writeaway/plugin-seo/src/styles";

Alternatively, include compiled CSS files

    @import "~@writeaway/core/dist/css/core.css";
    @import "~@writeaway/plugin-medium/dist/css/plugin-medium.css";
    @import "~@writeaway/plugin-seo/dist/css/plugin-seo.css";

## Integrating in React application

### Setup Store

WriteAway uses `redux` and `redux-thunk` middleware so you need to add `redux` provider on top level of your application

      import {
        defaultState as writeAwayState,
        reducerKey as writeAwayReducerKey,
        reducer as writeAwayReducer,
        IWriteAwayState
      } from '@writeaway/core';

      /**
       * Configure reducers
       */
      const reducers = combineReducers({
        // Required: Add WriteAway reducer
        [writeAwayReducerKey as '@writeaway']: writeAwayReducer as Reducer<IWriteAwayState>,
        // Options: Add toastr reducer if you use react-redux-toastr. If not, handle react-redux-toastr actions manually in your app to show toasts from WriteAway.
        toastr,
        // Add any other application specific reducers
        app: appReducer,
      });

      const defaultAppState = {};

      /**
       * Configure initial default state
       */
      const initialState: IApplicationState = {
        [writeAwayReducerKey]: writeAwayState as IWriteAwayState,
        toastr: undefined as any,
        app: defaultAppState,
      };

      // Compose middlewares
      const middlewares = [
        // WriteAway relies on thunk middleware
        thunk as ThunkMiddleware,
        // Add other middlewares if you need them
        // sagaMiddleware,
        // routerMiddleware(history)
      ];

      const enhancers = [applyMiddleware(...middlewares)];
      const store = createStore<IApplicationState, AnyAction, {}, unknown>(
        reducers as any,
        initialState,
        composeWithDevTools({
          name: 'WriteAway React Demo',
          serialize: {
            // If you are using redux devtools you may want to add this snippet to
            // serialize section as WriteAway stores node refences in store
            replacer: (key: string, value: any) => {
              if (value instanceof HTMLElement) { // use your custom data type checker
                return `HTMLElement:${value.tagName}`;
              }
              if (value && value.prototype && value.prototype.isReactComponent) { // use your custom data type checker
                return `IComponent:${(value as any).label}`;
              }
              return value;
            },
          } as any,
        })(...enhancers),
      );

    <>
        <Provider store={store}>
          <MyApplication />
        </Provider>
    </>

### Use React Pieces

When enabling edit mode, render editable pieces with react components from plugins and to render editors.

    import { ReactPieceSourceCode } from '@writeaway/plugin-codemirror';
    import { ReactPieceBlockBackground, ReactPieceImage, ReactPieceRichText } from '@writeaway/plugin-medium';
    import { ReactPieceSeo } from '@writeaway/plugin-seo';

    export const App = () => (
      <main>
              <ReactPieceBlockBackground
                id="bg-1"
                name="Background For Rich Text"
                bgColor="#FFcccc"
                className="p-4"
              >
                <article>
                  <h2>Rich Text Block</h2>
                  <ReactPieceRichText
                    id="rich-1"
                    name="Rich Text"
                    html="<div>Editable Rich Text</div>"
                  />
                </article>
              </ReactPieceBlockBackground>
              <ReactPieceBlockBackground
                id="bg-2"
                name="Background For Image"
                bgColor="#ccFFcc"
                className="p-4"
              >
                <article>
                  <h2>Image Block</h2>
                  <ReactPieceImage
                    id="image-1"
                    name="image"
                    src="https://writeaway.github.io/images/image-06.jpg"
                    title="Editable Image"
                    alt="Sample Image"
                  />
                </article>
              </ReactPieceBlockBackground>
              <ReactPieceBlockBackground
                id="bg-3"
                bgColor="#ccccff"
                name="Background For Source Code Block"
                className="p-4"
                src="https://writeaway.github.io/images/sayagata-400px.png"
              >
                <article>
                  <h2>Source Code Block</h2>
                  <ReactPieceSourceCode
                    id="source-1"
                    name="Source edit"
                    html={codeStr}
                    updateNode
                  />
                </article>
              </ReactPieceBlockBackground>
              <article className="p-4">
                <h2>SEO data block</h2>
                <div className="btn">
                  <ReactPieceSeo
                    id="seo-1"
                    name="SEO Data"
                    label="Click to Edit SEO Meta Data"
                    header="<meta></meta>"
                    title="Page Title"
                    description="Page Descriptions"
                    keywords="Page Keywords"
                  />
                </div>
              </article>
    </main>);

In the body, render navbar, toast controller, overlay and editors

            <WriteAwayOverlay />
            <WriteAwayNavBar options={
              {
                navBarDraggable: true,
                navBarCollapsable: true,
                pieceNameGroupSeparator: ':',
              }
            }
            />
            <WriteAwayEditors />
            <ReduxToastr
              className="r_toast-container"
              timeOut={4000}
              position="top-right"
            />

`WriteAwayEditors` is mandatory for editor functionalities as it renders popups and required overlays for editing inside.

`WriteAwayOverlay` is optional and renders hover effect for nodes

`WriteAwayNavBar` is optional and renders control panel that has switches for editors

`ReduxToastr` container is needed if you need to render redux toast messages from WriteAway

### Special Actions

Few special redux actions are exposed

        import {
          externalPieceUpdateAction, setAPIAction, setMetaAction
        } from '@writeaway/core';

externalPieceUpdateAction - explicitly updates data of specific node. You can use it for real-time updates of content from WebSockets connection. Note funtionality relies on `api.resolveConflicts` method that decides if update should be applied.

        // There is a server update of `source-1` node by John Doe
        dispatch(externalPieceUpdateAction(
            {
              id: 'source-1',
              data: { html: "<div>New HTML</div>" },
              meta: { id: 'user-b', label: 'John Doe', time: Date.now() }
            }
        ));

setAPIAction - sets pieces api dynamically. Usefull when you need to create HTTP instances asyncrously.

        // There is a server update of `source-1` node by John Doe
        dispatch(setAPIAction(
            {
              getPieceData: async (piece: IPieceItem) => fetchPieceFromServer(piece.id),
              /* ... */
            }
        ));

setMetaAction - sets piece meta, that will be attached to pieces updated in this WriteAway editing session. Typically that's info of user who is editing content.

        // There is a server update of `source-1` node by John Doe
        dispatch(setMetaAction(
            {
              id: 'user-a',
              label: 'John Smith',
            }
        ));

### Sample

See fully working React SPA sample here.

## WriteAwayCore constructor options

WriteAwayCore accepts IOptions object in constructor

Option | Default | Description
--- | --- | ---
api | defaultMinimumApi | data API to work with pieces. See details in WriteAway API section.
piecesOptions | defaultPieces | Options for pieces initialization
piecesOptions.selector | `[data-piece]` | Selector that will be looked for during initialization for auto-attaching to nodes
piecesOptions.attribute | `data-piece` | Attribute having `type` property for Piece initialization
piecesOptions.attributeId | `data-id` | Attribute having `id` property for Piece initialization
piecesOptions.attributeName | `data-name` | Attribute having `name` property for Piece initialization
piecesOptions.components | {} | Maps piece type to IComponent that are launched as piece editor
piecesOptions.options | {} | Maps piece type to options that will be passed to each of editor instances
piecesOptions.nameGroupSeparator | `,` | Name separator for piece names, i.e. if separator is ':' names like 'Body:Article' and 'Body:About' will be grouped under 'Body' with 'Article' and 'About' names
piecesRoot | `document.body` | DOM Element where nodes matching `piecesOptions.selector` will be searched for
editorRoot | `document.body` | DOM Element where editors can put their DOM components, i.e. modals and overlays
navBarRoot | `document.body` | DOM Element where floating navigation bar with core edit controls will be attached to
navBarCollapsible | `true` | If navbar can be collapsed
navBarCollapsed | `false` | Initial collapsed state for navbar
navBarDraggable | `true` | Can navbar be dragged
enableEdit | `true` | Initially enable editors or not
expert | `false` | Initially enable expert mode. Expert mode enables individual piece tracking in nav bar.
overlayRoot | `document.body` | DOM Element where floating hover overlay
ajax | `undefined` | If specified, will be passed to default callFetch helper implementation. Refer to code for details.
state | `undefined` | If specified, will be used as initial redux state

## WriteAway API

Specifying `api` params allows to customize where editors are taking data from and where data is saved. Additionally developer can customize how node position is calculated, see source code for that.

Option | Type | Description
--- | --- | ---
getPieceData | async (piece: IPieceItem) => IPieceItem | Async function to resolve complete piece data. Typically that means resolving `data` by `id` or extracting data directly from `node`
savePieceData | async (piece: IPieceItem) => void | Async function to save complete piece data
getImageList | async (ref: {id: string, type: string, data: any, dataset: any}) => Array< IGalleryItem > | Optional. If specified will fetch images for this piece and show image options
uploadImage | async (file: File or FileList) => IGalleryItem or Array< IGalleryItem > | Optional. If specified enable upload functionality
deleteImage | async (id: string) => void | Optional. If specified will enable image deletion functionality and will be used to delete images from gallery

## Usage with Spiral Framework

When using with spiral framework, use `@writeaway/spiral-bridge` bundle or pre-compiled scripts

See documentation here

## Developing and building

    yarn
    yarn build

## License

MIT

Generated using TypeDoc