From 4ee65760fe846188803302f44f6435cd537384d3 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 8 Feb 2021 22:39:15 +0000 Subject: [PATCH] Add BaseSequenceBlock superclass --- .../StreamField/blocks/BaseSequenceBlock.js | 217 ++++++++++++++++++ .../StreamField/blocks/ListBlock.js | 200 +--------------- .../StreamField/blocks/StreamBlock.js | 199 +--------------- 3 files changed, 224 insertions(+), 392 deletions(-) diff --git a/client/src/components/StreamField/blocks/BaseSequenceBlock.js b/client/src/components/StreamField/blocks/BaseSequenceBlock.js index c14c11f3d6..216f6e7cbd 100644 --- a/client/src/components/StreamField/blocks/BaseSequenceBlock.js +++ b/client/src/components/StreamField/blocks/BaseSequenceBlock.js @@ -1,3 +1,5 @@ +/* eslint-disable no-underscore-dangle */ + /* global $ */ import { escapeHtml as h } from '../../../utils/text'; @@ -151,3 +153,218 @@ export class BaseInsertionControl { $(this.element).hide().attr('aria-hidden', 'true'); } } + + +export class BaseSequenceBlock { + // eslint-disable-next-line no-unused-vars + _createChild(blockDef, placeholder, prefix, index, id, initialState, opts) { + throw new Error('not implemented'); + } + + // eslint-disable-next-line no-unused-vars + _createInsertionControl(placeholder, opts) { + throw new Error('not implemented'); + } + + // eslint-disable-next-line no-unused-vars + _getChildDataForInsertion(opts) { + throw new Error('not implemented'); + } + + clear() { + this.countInput.val(0); + this.sequenceContainer.empty(); + this.children = []; + this.blockCounter = 0; + + // Create initial insertion control + const placeholder = document.createElement('div'); + this.sequenceContainer.append(placeholder); + this.inserters = [ + this._createInsertionControl( + placeholder, { + index: 0, + onRequestInsert: (newIndex, opts) => { + this._onRequestInsert(newIndex, opts); + }, + strings: this.blockDef.meta.strings, + } + ) + ]; + } + + _onRequestInsert(index, opts) { + /* handler for an 'insert new block' action */ + const [blockDef, initialState] = this._getChildDataForInsertion(opts); + const newChild = this._insert(blockDef, initialState, null, index, { animate: true }); + newChild.focus(); + } + + + _insert(childBlockDef, initialState, id, index, opts) { + const prefix = this.prefix + '-' + this.blockCounter; + const animate = opts && opts.animate; + this.blockCounter++; + + /* + a new inserter and block will be inserted AFTER the inserter with the given index; + e.g if there are 3 blocks the children of sequenceContainer will be + [inserter 0, block 0, inserter 1, block 1, inserter 2, block 2, inserter 3] + and inserting a new block at index 1 will create a new block 1 and inserter 2 after the + current inserter 1, and increment everything after that point + */ + const existingInserterElement = this.inserters[index].element; + const blockPlaceholder = document.createElement('div'); + const inserterPlaceholder = document.createElement('div'); + $(blockPlaceholder).insertAfter(existingInserterElement); + $(inserterPlaceholder).insertAfter(blockPlaceholder); + + /* shuffle up indexes of all blocks / inserters above this index */ + for (let i = index; i < this.children.length; i++) { + this.children[i].setIndex(i + 1); + } + for (let i = index + 1; i < this.inserters.length; i++) { + this.inserters[i].setIndex(i + 1); + } + + const child = this._createChild(childBlockDef, blockPlaceholder, prefix, index, id, initialState, { + animate, + onRequestDuplicate: (i) => { this.duplicateBlock(i); }, + onRequestDelete: (i) => { this.deleteBlock(i); }, + onRequestMoveUp: (i) => { this.moveBlock(i, i - 1); }, + onRequestMoveDown: (i) => { this.moveBlock(i, i + 1); }, + strings: this.blockDef.meta.strings, + }); + this.children.splice(index, 0, child); + + const inserter = this._createInsertionControl( + inserterPlaceholder, { + index: index + 1, + onRequestInsert: (newIndex, inserterOpts) => { + this._onRequestInsert(newIndex, inserterOpts); + }, + strings: this.blockDef.meta.strings, + } + ); + this.inserters.splice(index + 1, 0, inserter); + + this.countInput.val(this.blockCounter); + + const isFirstChild = (index === 0); + const isLastChild = (index === this.children.length - 1); + if (!isFirstChild) { + child.enableMoveUp(); + if (isLastChild) { + /* previous child (which was previously the last one) can now move down */ + this.children[index - 1].enableMoveDown(); + } + } + if (!isLastChild) { + child.enableMoveDown(); + if (isFirstChild) { + /* next child (which was previously the first one) can now move up */ + this.children[index + 1].enableMoveUp(); + } + } + + return child; + } + + deleteBlock(index) { + this.children[index].markDeleted({ animate: true }); + this.inserters[index].delete(); + this.children.splice(index, 1); + this.inserters.splice(index, 1); + + /* index numbers of children / inserters above this index now need updating to match + their array indexes */ + for (let i = index; i < this.children.length; i++) { + this.children[i].setIndex(i); + } + for (let i = index; i < this.inserters.length; i++) { + this.inserters[i].setIndex(i); + } + + if (index === 0 && this.children.length > 0) { + /* we have removed the first child; the new first child cannot be moved up */ + this.children[0].disableMoveUp(); + } + if (index === this.children.length && this.children.length > 0) { + /* we have removed the last child; the new last child cannot be moved down */ + this.children[this.children.length - 1].disableMoveDown(); + } + } + + moveBlock(oldIndex, newIndex) { + if (oldIndex === newIndex) return; + const inserterToMove = this.inserters[oldIndex]; + const childToMove = this.children[oldIndex]; + + /* move HTML elements */ + if (newIndex > oldIndex) { + $(inserterToMove.element).insertAfter(this.children[newIndex].element); + } else { + $(inserterToMove.element).insertBefore(this.inserters[newIndex].element); + } + $(childToMove.element).insertAfter(inserterToMove.element); + + /* reorder items in the `inserters` and `children` arrays */ + this.inserters.splice(oldIndex, 1); + this.inserters.splice(newIndex, 0, inserterToMove); + this.children.splice(oldIndex, 1); + this.children.splice(newIndex, 0, childToMove); + + /* update index properties of moved items */ + if (newIndex > oldIndex) { + for (let i = oldIndex; i <= newIndex; i++) { + this.inserters[i].setIndex(i); + this.children[i].setIndex(i); + } + } else { + for (let i = newIndex; i <= oldIndex; i++) { + this.inserters[i].setIndex(i); + this.children[i].setIndex(i); + } + } + + /* enable/disable up/down arrows as required */ + const maxIndex = this.children.length - 1; + if (oldIndex === 0) { + childToMove.enableMoveUp(); + this.children[0].disableMoveUp(); + } + if (oldIndex === maxIndex) { + childToMove.enableMoveDown(); + this.children[maxIndex].disableMoveDown(); + } + if (newIndex === 0) { + childToMove.disableMoveUp(); + this.children[1].enableMoveUp(); + } + if (newIndex === maxIndex) { + childToMove.disableMoveDown(); + this.children[maxIndex - 1].enableMoveDown(); + } + } + + setState(values) { + this.clear(); + values.forEach((val, i) => { + this.insert(val, i); + }); + } + + getState() { + return this.children.map(child => child.getState()); + } + + getValue() { + return this.children.map(child => child.getValue()); + } + + focus() { + if (this.children.length) { + this.children[0].focus(); + } + } +} diff --git a/client/src/components/StreamField/blocks/ListBlock.js b/client/src/components/StreamField/blocks/ListBlock.js index 721ae2c703..fb706eac9e 100644 --- a/client/src/components/StreamField/blocks/ListBlock.js +++ b/client/src/components/StreamField/blocks/ListBlock.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ -import { BaseSequenceChild, BaseInsertionControl } from './BaseSequenceBlock'; +import { BaseSequenceBlock, BaseSequenceChild, BaseInsertionControl } from './BaseSequenceBlock'; import { escapeHtml as h } from '../../../utils/text'; /* global $ */ @@ -51,7 +51,7 @@ class InsertPosition extends BaseInsertionControl { } } -export class ListBlock { +export class ListBlock extends BaseSequenceBlock { constructor(blockDef, placeholder, prefix, initialState, initialError) { this.blockDef = blockDef; this.type = blockDef.name; @@ -81,7 +81,7 @@ export class ListBlock { this.inserters = []; this.blockCounter = 0; this.countInput = dom.find('[data-streamfield-list-count]'); - this.listContainer = dom.find('[data-streamfield-list-container]'); + this.sequenceContainer = dom.find('[data-streamfield-list-container]'); this.setState(initialState || []); if (initialError) { @@ -89,28 +89,6 @@ export class ListBlock { } } - clear() { - this.countInput.val(0); - this.listContainer.empty(); - this.children = []; - this.blockCounter = 0; - - // Create initial insert position - const placeholder = document.createElement('div'); - this.listContainer.append(placeholder); - this.inserters = [ - this._createInsertionControl( - placeholder, { - index: 0, - onRequestInsert: (newIndex, opts) => { - this._onRequestInsert(newIndex, opts); - }, - strings: this.blockDef.meta.strings, - } - ) - ]; - } - _getChildDataForInsertion() { /* Called when an 'insert new block' action is triggered: given a dict of data from the insertion control, return the block definition and initial state to be used for the new block. @@ -121,13 +99,6 @@ export class ListBlock { return [blockDef, initialState]; } - _onRequestInsert(index, opts) { - /* handler for an 'insert new block' action */ - const [blockDef, initialState] = this._getChildDataForInsertion(opts); - const newChild = this._insert(blockDef, initialState, null, index, { animate: true }); - newChild.focus(); - } - _createChild(blockDef, placeholder, prefix, index, id, initialState, opts) { return new ListChild(blockDef, placeholder, prefix, index, id, initialState, opts); } @@ -140,163 +111,12 @@ export class ListBlock { return this._insert(this.blockDef.childBlockDef, value, null, index, opts); } - _insert(childBlockDef, initialState, id, index, opts) { - const prefix = this.prefix + '-' + this.blockCounter; - const animate = opts && opts.animate; - this.blockCounter++; - - /* - a new insert position and block will be inserted AFTER the insert position with the given index; - e.g if there are 3 blocks the children of listContainer will be - [insert pos 0, block 0, insert pos 1, block 1, insert pos 2, block 2, insert pos 3] - and inserting a new block at index 1 will create a new block 1 and insert position 2 after the - current menu 1, and increment everything after that point - */ - const existingInsertPositionElement = this.inserters[index].element; - const blockPlaceholder = document.createElement('div'); - const inserterPlaceholder = document.createElement('div'); - $(blockPlaceholder).insertAfter(existingInsertPositionElement); - $(inserterPlaceholder).insertAfter(blockPlaceholder); - - /* shuffle up indexes of all blocks / insert positions above this index */ - for (let i = index; i < this.children.length; i++) { - this.children[i].setIndex(i + 1); - } - for (let i = index + 1; i < this.inserters.length; i++) { - this.inserters[i].setIndex(i + 1); - } - - const child = this._createChild(childBlockDef, blockPlaceholder, prefix, index, id, initialState, { - animate, - onRequestDuplicate: (i) => { this.duplicateBlock(i); }, - onRequestDelete: (i) => { this.deleteBlock(i); }, - onRequestMoveUp: (i) => { this.moveBlock(i, i - 1); }, - onRequestMoveDown: (i) => { this.moveBlock(i, i + 1); }, - strings: this.blockDef.meta.strings, - }); - this.children.splice(index, 0, child); - - const inserter = this._createInsertionControl( - inserterPlaceholder, { - index: index + 1, - onRequestInsert: (newIndex, inserterOpts) => { - this._onRequestInsert(newIndex, inserterOpts); - }, - strings: this.blockDef.meta.strings, - } - ); - this.inserters.splice(index + 1, 0, inserter); - - this.countInput.val(this.blockCounter); - - const isFirstChild = (index === 0); - const isLastChild = (index === this.children.length - 1); - if (!isFirstChild) { - child.enableMoveUp(); - if (isLastChild) { - /* previous child (which was previously the last one) can now move down */ - this.children[index - 1].enableMoveDown(); - } - } - if (!isLastChild) { - child.enableMoveDown(); - if (isFirstChild) { - /* next child (which was previously the first one) can now move up */ - this.children[index + 1].enableMoveUp(); - } - } - } - duplicateBlock(index) { const childState = this.children[index].getState(); this.insert(childState, index + 1, { animate: true }); this.children[index + 1].focus(); } - deleteBlock(index) { - this.children[index].markDeleted({ animate: true }); - this.inserters[index].delete(); - this.children.splice(index, 1); - this.inserters.splice(index, 1); - - /* index numbers of children / insert positions above this index now need updating to match - their array indexes */ - for (let i = index; i < this.children.length; i++) { - this.children[i].setIndex(i); - } - for (let i = index; i < this.inserters.length; i++) { - this.inserters[i].setIndex(i); - } - - if (index === 0 && this.children.length > 0) { - /* we have removed the first child; the new first child cannot be moved up */ - this.children[0].disableMoveUp(); - } - if (index === this.children.length && this.children.length > 0) { - /* we have removed the last child; the new last child cannot be moved down */ - this.children[this.children.length - 1].disableMoveDown(); - } - } - - moveBlock(oldIndex, newIndex) { - if (oldIndex === newIndex) return; - const inserterToMove = this.inserters[oldIndex]; - const childToMove = this.children[oldIndex]; - - /* move HTML elements */ - if (newIndex > oldIndex) { - $(inserterToMove.element).insertAfter(this.children[newIndex].element); - } else { - $(inserterToMove.element).insertBefore(this.inserters[newIndex].element); - } - $(childToMove.element).insertAfter(inserterToMove.element); - - /* reorder items in the `insert positions` and `children` arrays */ - this.inserters.splice(oldIndex, 1); - this.inserters.splice(newIndex, 0, inserterToMove); - this.children.splice(oldIndex, 1); - this.children.splice(newIndex, 0, childToMove); - - /* update index properties of moved items */ - if (newIndex > oldIndex) { - for (let i = oldIndex; i <= newIndex; i++) { - this.inserters[i].setIndex(i); - this.children[i].setIndex(i); - } - } else { - for (let i = newIndex; i <= oldIndex; i++) { - this.inserters[i].setIndex(i); - this.children[i].setIndex(i); - } - } - - /* enable/disable up/down arrows as required */ - const maxIndex = this.children.length - 1; - if (oldIndex === 0) { - childToMove.enableMoveUp(); - this.children[0].disableMoveUp(); - } - if (oldIndex === maxIndex) { - childToMove.enableMoveDown(); - this.children[maxIndex].disableMoveDown(); - } - if (newIndex === 0) { - childToMove.disableMoveUp(); - this.children[1].enableMoveUp(); - } - if (newIndex === maxIndex) { - childToMove.disableMoveDown(); - this.children[maxIndex - 1].enableMoveDown(); - } - } - - setState(values) { - this.clear(); - values.forEach(val => { - this.insert(val, this.children.length); - }); - } - setError(errorList) { if (errorList.length !== 1) { return; @@ -310,20 +130,6 @@ export class ListBlock { } } } - - getState() { - return this.children.map(child => child.getState()); - } - - getValue() { - return this.children.map(child => child.getValue()); - } - - focus() { - if (this.children.length) { - this.children[0].focus(); - } - } } export class ListBlockDefinition { diff --git a/client/src/components/StreamField/blocks/StreamBlock.js b/client/src/components/StreamField/blocks/StreamBlock.js index 45454622f3..a88fa69df0 100644 --- a/client/src/components/StreamField/blocks/StreamBlock.js +++ b/client/src/components/StreamField/blocks/StreamBlock.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ -import { BaseSequenceChild, BaseInsertionControl } from './BaseSequenceBlock'; +import { BaseSequenceBlock, BaseSequenceChild, BaseInsertionControl } from './BaseSequenceBlock'; import { escapeHtml as h } from '../../../utils/text'; /* global $ */ @@ -126,7 +126,7 @@ class StreamBlockMenu extends BaseInsertionControl { } } -export class StreamBlock { +export class StreamBlock extends BaseSequenceBlock { constructor(blockDef, placeholder, prefix, initialState, initialError) { this.blockDef = blockDef; this.type = blockDef.name; @@ -167,7 +167,7 @@ export class StreamBlock { // Parent element of insert control and block elements (potentially including deleted items, // which are left behind as hidden elements with a '-deleted' input so that the // server-side form handler knows to skip it) - this.streamContainer = dom.find('[data-streamfield-stream-container]'); + this.sequenceContainer = dom.find('[data-streamfield-stream-container]'); this.setState(initialState || []); this.container = dom; @@ -176,27 +176,6 @@ export class StreamBlock { } } - clear() { - this.countInput.val(0); - this.streamContainer.empty(); - this.children = []; - this.blockCounter = 0; - - const placeholder = document.createElement('div'); - this.streamContainer.append(placeholder); - this.inserters = [ - this._createInsertionControl( - placeholder, { - index: 0, - onRequestInsert: (index, opts) => { - this._onRequestInsert(index, opts); - }, - strings: this.blockDef.meta.strings, - } - ) - ]; - } - _createChild(blockDef, placeholder, prefix, index, id, initialState, opts) { return new StreamChild(blockDef, placeholder, prefix, index, id, initialState, opts); } @@ -212,75 +191,6 @@ export class StreamBlock { return this._insert(childBlockDef, value, id, index, opts); } - _insert(childBlockDef, initialState, id, index, opts) { - const prefix = this.prefix + '-' + this.blockCounter; - const animate = opts && opts.animate; - this.blockCounter++; - - /* - a new inserter and block will be inserted AFTER the inserter with the given index; - e.g if there are 3 blocks the children of streamContainer will be - [inserter 0, block 0, inserter 1, block 1, inserter 2, block 2, inserter 3] - and inserting a new block at index 1 will create a new block 1 and inserter 2 after the - current inserter 1, and increment everything after that point - */ - const existingMenuElement = this.inserters[index].element; - const blockPlaceholder = document.createElement('div'); - const inserterPlaceholder = document.createElement('div'); - $(blockPlaceholder).insertAfter(existingMenuElement); - $(inserterPlaceholder).insertAfter(blockPlaceholder); - - /* shuffle up indexes of all blocks / inserters above this index */ - for (let i = index; i < this.children.length; i++) { - this.children[i].setIndex(i + 1); - } - for (let i = index + 1; i < this.inserters.length; i++) { - this.inserters[i].setIndex(i + 1); - } - - const child = this._createChild(childBlockDef, blockPlaceholder, prefix, index, id, initialState, { - animate, - onRequestDuplicate: (i) => { this.duplicateBlock(i); }, - onRequestDelete: (i) => { this.deleteBlock(i); }, - onRequestMoveUp: (i) => { this.moveBlock(i, i - 1); }, - onRequestMoveDown: (i) => { this.moveBlock(i, i + 1); }, - strings: this.blockDef.meta.strings, - }); - this.children.splice(index, 0, child); - - const inserter = this._createInsertionControl( - inserterPlaceholder, { - index: index + 1, - onRequestInsert: (newIndex, inserterOpts) => { - this._onRequestInsert(newIndex, inserterOpts); - }, - strings: this.blockDef.meta.strings, - } - ); - this.inserters.splice(index + 1, 0, inserter); - - this.countInput.val(this.blockCounter); - - const isFirstChild = (index === 0); - const isLastChild = (index === this.children.length - 1); - if (!isFirstChild) { - child.enableMoveUp(); - if (isLastChild) { - /* previous child (which was previously the last one) can now move down */ - this.children[index - 1].enableMoveDown(); - } - } - if (!isLastChild) { - child.enableMoveDown(); - if (isFirstChild) { - /* next child (which was previously the first one) can now move up */ - this.children[index + 1].enableMoveUp(); - } - } - - return child; - } - _getChildDataForInsertion({ type }) { /* Called when an 'insert new block' action is triggered: given a dict of data from the insertion control, return the block definition and initial state to be used for the new block. @@ -291,13 +201,6 @@ export class StreamBlock { return [blockDef, initialState]; } - _onRequestInsert(index, opts) { - /* handler for an 'insert new block' action */ - const [blockDef, initialState] = this._getChildDataForInsertion(opts); - const newChild = this._insert(blockDef, initialState, null, index, { animate: true }); - newChild.focus(); - } - duplicateBlock(index) { const childState = this.children[index].getState(); childState.id = null; @@ -305,88 +208,8 @@ export class StreamBlock { this.children[index + 1].focus(); } - deleteBlock(index) { - this.children[index].markDeleted({ animate: true }); - this.inserters[index].delete(); - this.children.splice(index, 1); - this.inserters.splice(index, 1); - - /* index numbers of children / inserters above this index now need updating to match - their array indexes */ - for (let i = index; i < this.children.length; i++) { - this.children[i].setIndex(i); - } - for (let i = index; i < this.inserters.length; i++) { - this.inserters[i].setIndex(i); - } - - if (index === 0 && this.children.length > 0) { - /* we have removed the first child; the new first child cannot be moved up */ - this.children[0].disableMoveUp(); - } - if (index === this.children.length && this.children.length > 0) { - /* we have removed the last child; the new last child cannot be moved down */ - this.children[this.children.length - 1].disableMoveDown(); - } - } - - moveBlock(oldIndex, newIndex) { - if (oldIndex === newIndex) return; - const inserterToMove = this.inserters[oldIndex]; - const childToMove = this.children[oldIndex]; - - /* move HTML elements */ - if (newIndex > oldIndex) { - $(inserterToMove.element).insertAfter(this.children[newIndex].element); - } else { - $(inserterToMove.element).insertBefore(this.inserters[newIndex].element); - } - $(childToMove.element).insertAfter(inserterToMove.element); - - /* reorder items in the `inserters` and `children` arrays */ - this.inserters.splice(oldIndex, 1); - this.inserters.splice(newIndex, 0, inserterToMove); - this.children.splice(oldIndex, 1); - this.children.splice(newIndex, 0, childToMove); - - /* update index properties of moved items */ - if (newIndex > oldIndex) { - for (let i = oldIndex; i <= newIndex; i++) { - this.inserters[i].setIndex(i); - this.children[i].setIndex(i); - } - } else { - for (let i = newIndex; i <= oldIndex; i++) { - this.inserters[i].setIndex(i); - this.children[i].setIndex(i); - } - } - - /* enable/disable up/down arrows as required */ - const maxIndex = this.children.length - 1; - if (oldIndex === 0) { - childToMove.enableMoveUp(); - this.children[0].disableMoveUp(); - } - if (oldIndex === maxIndex) { - childToMove.enableMoveDown(); - this.children[maxIndex].disableMoveDown(); - } - if (newIndex === 0) { - childToMove.disableMoveUp(); - this.children[1].enableMoveUp(); - } - if (newIndex === maxIndex) { - childToMove.disableMoveDown(); - this.children[maxIndex - 1].enableMoveDown(); - } - } - setState(values) { - this.clear(); - values.forEach((val, i) => { - this.insert(val, i); - }); + super.setState(values); if (values.length === 0) { /* for an empty list, begin with the menu open */ this.inserters[0].open({ animate: false }); @@ -422,20 +245,6 @@ export class StreamBlock { } } } - - getState() { - return this.children.map(child => child.getState()); - } - - getValue() { - return this.children.map(child => child.getValue()); - } - - focus() { - if (this.children.length) { - this.children[0].focus(); - } - } } export class StreamBlockDefinition {