From 17cfde67ce5cff9e86461f7d807b2dda9bbfcd2b Mon Sep 17 00:00:00 2001 From: AlbertHilb Date: Tue, 11 Jul 2017 22:41:13 +0200 Subject: [PATCH] Add `data` field to editor `State`. (#830) * Add `data` field to editor `State`. Plugins can use it to store their own internal state. * Remove `serialize` and `deserialize` plugin methods. * Add operation to set `data` on a state. * Add `setDataOperation` tests. * Remove the possibility to use keys different from strings. Add `preserveData` option to `raw.serialize`. Rewrite `set-data` test exploiting the new option. --- src/models/state.js | 16 ++++++++-- src/serializers/raw.js | 29 +++++++++++------- src/transforms/apply-operation.js | 21 ++++++++++++- src/transforms/operations.js | 30 +++++++++++++++++++ .../fixtures/state-data/set-data/index.js | 14 +++++++++ .../fixtures/state-data/set-data/input.yaml | 12 ++++++++ .../fixtures/state-data/set-data/output.yaml | 16 ++++++++++ test/transforms/index.js | 21 +++++++++++++ 8 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 test/transforms/fixtures/state-data/set-data/index.js create mode 100644 test/transforms/fixtures/state-data/set-data/input.yaml create mode 100644 test/transforms/fixtures/state-data/set-data/output.yaml diff --git a/src/models/state.js b/src/models/state.js index 10408dd8e..638febf34 100644 --- a/src/models/state.js +++ b/src/models/state.js @@ -4,7 +4,7 @@ import Document from './document' import SCHEMA from '../schemas/core' import Selection from './selection' import Transform from './transform' -import { Record, Set, Stack, List } from 'immutable' +import { Record, Set, Stack, List, Map } from 'immutable' /** * History. @@ -27,6 +27,7 @@ const DEFAULTS = { document: new Document(), selection: new Selection(), history: new History(), + data: new Map(), isNative: false } @@ -52,13 +53,24 @@ class State extends new Record(DEFAULTS) { const document = Document.create(properties.document) let selection = Selection.create(properties.selection) + let data = new Map() if (selection.isUnset) { const text = document.getFirstText() selection = selection.collapseToStartOf(text) } - const state = new State({ document, selection }) + // Set default value for `data`. + if (options.plugins) { + for (const plugin of options.plugins) { + if (plugin.data) data = data.merge(plugin.data) + } + } + + // Then add data provided in `properties`. + if (properties.data) data = data.merge(properties.data) + + const state = new State({ document, selection, data }) return options.normalize === false ? state diff --git a/src/serializers/raw.js b/src/serializers/raw.js index 55e69e38b..b83f7aa52 100644 --- a/src/serializers/raw.js +++ b/src/serializers/raw.js @@ -184,7 +184,7 @@ const Raw = { selection = Raw.deserializeSelection(object.selection, options) } - return State.create({ document, selection }, options) + return State.create({ data: object.data, document, selection }, options) }, /** @@ -400,12 +400,15 @@ const Raw = { serializeState(state, options = {}) { const object = { document: Raw.serializeDocument(state.document, options), - selection: Raw.serializeSelection(state.selection, options), kind: state.kind } - if (!options.preserveSelection) { - delete object.selection + if (options.preserveSelection) { + object.selection = Raw.serializeSelection(state.selection, options) + } + + if (options.preserveStateData) { + object.data = state.data.toJSON() } const ret = options.terse @@ -546,14 +549,17 @@ const Raw = { */ tersifyState(object) { - if (object.selection == null) { - return object.document + const { data, document, selection } = object + const emptyData = isEmpty(data) + + if (!selection && emptyData) { + return document } - return { - document: object.document, - selection: object.selection - } + const ret = { document } + if (!emptyData) ret.data = data + if (selection) ret.selection = selection + return ret }, /** @@ -673,9 +679,10 @@ const Raw = { */ untersifyState(object) { - if (object.selection || object.document) { + if (object.document) { return { kind: 'state', + data: object.data, document: object.document, selection: object.selection, } diff --git a/src/transforms/apply-operation.js b/src/transforms/apply-operation.js index b55d839b4..d9a60a550 100644 --- a/src/transforms/apply-operation.js +++ b/src/transforms/apply-operation.js @@ -40,7 +40,9 @@ const OPERATIONS = { set_node: setNode, split_node: splitNode, // Selection operations. - set_selection: setSelection + set_selection: setSelection, + // State data operations. + set_data: setData } /** @@ -334,6 +336,23 @@ function removeText(state, operation) { return state } +/** + * Set `data` on `state`. + * + * @param {State} state + * @param {Object} operation + * @return {State} + */ + +function setData(state, operation) { + const { properties } = operation + let { data } = state + + data = data.merge(properties) + state = state.set('data', data) + return state +} + /** * Set `properties` on mark on text at `offset` and `length` in node by `path`. * diff --git a/src/transforms/operations.js b/src/transforms/operations.js index ab97bdffa..f6d60ad2b 100644 --- a/src/transforms/operations.js +++ b/src/transforms/operations.js @@ -285,6 +285,36 @@ Transforms.removeTextOperation = (transform, path, offset, length) => { transform.applyOperation(operation) } +/** + * Merge `properties` into state `data`. + * + * @param {Transform} transform + * @param {Object} properties + */ + +Transforms.setDataOperation = (transform, properties) => { + const { state } = transform + const { data } = state + const inverseProps = {} + + for (const k in properties) { + inverseProps[k] = data[k] + } + + const inverse = [{ + type: 'set_data', + properties: inverseProps + }] + + const operation = { + type: 'set_data', + properties, + inverse, + } + + transform.applyOperation(operation) +} + /** * Set `properties` on mark on text at `offset` and `length` in node by `path`. * diff --git a/test/transforms/fixtures/state-data/set-data/index.js b/test/transforms/fixtures/state-data/set-data/index.js new file mode 100644 index 000000000..9680f9988 --- /dev/null +++ b/test/transforms/fixtures/state-data/set-data/index.js @@ -0,0 +1,14 @@ + +export default function (state) { + const data = { + key1: "value1", + key2: "value2" + } + + const next = state + .transform() + .setDataOperation(data) + .apply() + + return next +} diff --git a/test/transforms/fixtures/state-data/set-data/input.yaml b/test/transforms/fixtures/state-data/set-data/input.yaml new file mode 100644 index 000000000..881deb1d9 --- /dev/null +++ b/test/transforms/fixtures/state-data/set-data/input.yaml @@ -0,0 +1,12 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word + - kind: block + type: paragraph + nodes: + - kind: text + text: another diff --git a/test/transforms/fixtures/state-data/set-data/output.yaml b/test/transforms/fixtures/state-data/set-data/output.yaml new file mode 100644 index 000000000..77fc41a16 --- /dev/null +++ b/test/transforms/fixtures/state-data/set-data/output.yaml @@ -0,0 +1,16 @@ + +data: + key1: value1 + key2: value2 +document: + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word + - kind: block + type: paragraph + nodes: + - kind: text + text: another diff --git a/test/transforms/index.js b/test/transforms/index.js index 87f9905bd..f68bbf654 100644 --- a/test/transforms/index.js +++ b/test/transforms/index.js @@ -177,4 +177,25 @@ describe('transforms', async () => { }) } }) + + describe('state-data', () => { + const dir = resolve(__dirname, './fixtures/state-data') + const tests = fs.readdirSync(dir) + + for (const test of tests) { + if (test[0] == '.') continue + + it(test, async () => { + const testDir = resolve(dir, test) + const fn = require(testDir).default + const input = await readYaml(resolve(testDir, 'input.yaml')) + const expected = await readYaml(resolve(testDir, 'output.yaml')) + + let state = Raw.deserialize(input, { terse: true }) + state = fn(state) + const output = Raw.serialize(state, { terse: true, preserveStateData: true }) + assert.deepEqual(strip(output), strip(expected)) + }) + } + }) })