usfm-editor
Version:
A WYSIWYG editor component for USFM
405 lines (305 loc) • 15.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createBasicUsfmEditor = exports.BasicUsfmEditor = void 0;
var React = _interopRequireWildcard(require("react"));
var _slateReact = require("slate-react");
var _slate = require("slate");
var _usfmRenderer = require("../transforms/usfmRenderer");
var _usfmToSlate = require("../transforms/usfmToSlate");
var _normalizeNode = require("../plugins/normalizeNode");
var _keyHandlers = require("../plugins/keyHandlers");
var _slateToUsfm = require("../transforms/slateToUsfm");
var _lodash = require("lodash");
var _MyTransforms = require("../plugins/helpers/MyTransforms");
var _SelectionTransforms = require("../plugins/helpers/SelectionTransforms");
var _identificationTransforms = require("../transforms/identificationTransforms");
var _MyEditor = require("../plugins/helpers/MyEditor");
var _UsfmEditor = require("../UsfmEditor");
var _NodeRules = _interopRequireDefault(require("../utils/NodeRules"));
var _UsfmMarkers = require("../utils/UsfmMarkers");
var _NodeTypes = _interopRequireDefault(require("../utils/NodeTypes"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const createBasicUsfmEditor = () => {
const e = /*#__PURE__*/React.forwardRef(({ ...props
}, ref) => /*#__PURE__*/React.createElement(BasicUsfmEditor, _extends({}, props, {
ref: ref
})));
e.displayName = "BasicUsfmEditor";
return e;
};
/**
* A WYSIWYG editor component for USFM
*/
exports.createBasicUsfmEditor = createBasicUsfmEditor;
class BasicUsfmEditor extends React.Component {
constructor(props) {
super(props);
_defineProperty(this, "slateEditor", void 0);
_defineProperty(this, "getMarksAtSelection", () => {
if (!this.slateEditor.selection) return [];
const markObject = _slate.Editor.marks(this.slateEditor);
if (!markObject) return [];
const markArray = Object.keys(markObject).filter(k => markObject[k] === true);
return markArray;
});
_defineProperty(this, "addMarkAtSelection", mark => {
if (!this.slateEditor.selection) return;
_slate.Editor.addMark(this.slateEditor, mark, true);
});
_defineProperty(this, "removeMarkAtSelection", mark => {
if (!this.slateEditor.selection) return;
_slate.Editor.removeMark(this.slateEditor, mark);
});
_defineProperty(this, "getParagraphTypesAtSelection", () => {
if (!this.slateEditor.selection) return [];
let types = [];
for (const entry of _slate.Editor.nodes(this.slateEditor)) {
const node = entry[0];
if (_slate.Element.isElement(node) && (_UsfmMarkers.UsfmMarkers.isParagraphType(node) || node.type == _NodeTypes.default.INLINE_CONTAINER)) {
types = types.concat(node.type);
}
}
return types;
});
_defineProperty(this, "setParagraphTypeAtSelection", marker => {
if (!this.slateEditor.selection) return;
_slate.Transforms.setNodes(this.slateEditor, {
type: marker
}, {
match: _NodeRules.default.isFormattableBlockType
});
});
_defineProperty(this, "goToVerse", verseObject => {
if (!verseObject) return;
const {
chapter,
verse
} = verseObject;
const versePath = _MyEditor.MyEditor.findVersePath(this.slateEditor, chapter, verse);
if (!versePath) return;
const [verseNode] = _slate.Editor.node(this.slateEditor, versePath);
if (!_slate.Element.isElement(verseNode)) return;
const verseNumOrRange = _slate.Node.string(verseNode.children[0]);
const inlineContainerPath = versePath.concat(1);
_SelectionTransforms.SelectionTransforms.moveToStartOfFirstLeaf(this.slateEditor, inlineContainerPath);
_slateReact.ReactEditor.focus(this.slateEditor);
if (!this.props.onVerseChange || !this.didSelectedVerseChange(chapter, verseNumOrRange)) {
return;
}
this.updateSelectedVerse(chapter, verseNumOrRange);
});
_defineProperty(this, "handleChange", value => {
console.debug("after change", value);
this.fixSelectionOnChapterOrVerseNumber();
this.setState({
value: value
});
this.scheduleOnChange(value);
});
_defineProperty(this, "scheduleOnChange", (0, _lodash.debounce)(newValue => {
if (this.props.onChange) {
const usfm = (0, _slateToUsfm.slateToUsfm)(newValue);
this.props.onChange(usfm);
}
if (this.props.onVerseChange) {
// No need to keep track of selected chapter and verse if
// onVerseChange is not given.
this.updateSelectedVerseAfterEditorChange();
}
}, 200));
_defineProperty(this, "onKeyDown", event => {
(0, _keyHandlers.handleKeyPress)(event, this.slateEditor);
});
_defineProperty(this, "updateIdentificationFromProp", () => {
if (!this.props.identification) return;
const current = _MyEditor.MyEditor.identification(this.slateEditor);
const validUpdates = this.filterAndNormalize(this.props.identification);
const updated = (0, _identificationTransforms.mergeIdentification)(current, validUpdates);
if (!(0, _lodash.isEqual)(updated, current)) {
_MyTransforms.MyTransforms.setIdentification(this.slateEditor, updated);
if (this.props.onIdentificationChange) {
this.props.onIdentificationChange(updated);
}
}
});
_defineProperty(this, "updateIdentificationFromUsfm", () => {
const parsedIdentification = (0, _identificationTransforms.parseIdentificationFromUsfm)(this.props.usfmString);
const validParsed = this.filterAndNormalize(parsedIdentification);
_MyTransforms.MyTransforms.setIdentification(this.slateEditor, validParsed);
if (this.props.onIdentificationChange) {
this.props.onIdentificationChange(validParsed);
}
});
_defineProperty(this, "updateIdentificationFromUsfmAndProp", () => {
var _this$props$identific;
const parsedIdentification = (0, _identificationTransforms.parseIdentificationFromUsfm)(this.props.usfmString);
const validParsed = this.filterAndNormalize(parsedIdentification);
const updateIdentification = (_this$props$identific = this.props.identification) !== null && _this$props$identific !== void 0 ? _this$props$identific : {};
const validUpdates = this.filterAndNormalize(updateIdentification);
const updated = (0, _identificationTransforms.mergeIdentification)(validParsed, validUpdates);
_MyTransforms.MyTransforms.setIdentification(this.slateEditor, updated);
if (this.props.onIdentificationChange) {
this.props.onIdentificationChange(updated);
}
});
_defineProperty(this, "filterAndNormalize", ids => {
const filtered = (0, _identificationTransforms.filterInvalidIdentification)(ids);
return (0, _identificationTransforms.normalizeIdentificationValues)(filtered);
});
_defineProperty(this, "updateSelectedVerseAfterEditorChange", () => {
var _MyEditor$getChapterN;
if (!this.slateEditor.selection) return;
const verseNodeEntry = _MyEditor.MyEditor.getVerseNode(this.slateEditor);
if (!verseNodeEntry) return;
const [verseNode] = verseNodeEntry;
const chapterNode = (_MyEditor$getChapterN = _MyEditor.MyEditor.getChapterNode(this.slateEditor)) === null || _MyEditor$getChapterN === void 0 ? void 0 : _MyEditor$getChapterN[0];
if (!_slate.Element.isElement(chapterNode) || !_slate.Element.isElement(verseNode)) return;
const chapterNum = parseInt(_slate.Node.string(chapterNode.children[0]));
const verseNumOrRangeStr = _slate.Node.string(verseNode.children[0]);
if (this.didSelectedVerseChange(chapterNum, verseNumOrRangeStr)) {
this.updateSelectedVerse(chapterNum, verseNumOrRangeStr);
}
});
this.state = {
value: (0, _usfmToSlate.usfmToSlate)(props.usfmString),
selectedVerse: undefined,
prevUsfmStringProp: props.usfmString
};
this.slateEditor = (0, _lodash.flowRight)(_keyHandlers.withBackspace, _keyHandlers.withDelete, _keyHandlers.withEnter, _normalizeNode.withNormalize, _slateReact.withReact, _slate.createEditor)();
this.slateEditor.isInline = () => false;
} // Since usfmString and goToVerse can be updated at the same time, we need to calculate the new
// value before the editor renders. Once it renders, we can proceed.
static getDerivedStateFromProps(props, state) {
if (state.prevUsfmStringProp == props.usfmString) return null;
return {
value: (0, _usfmToSlate.usfmToSlate)(props.usfmString),
prevUsfmStringProp: props.usfmString
};
}
/* UsfmEditor interface functions */
fixSelectionOnChapterOrVerseNumber() {
var _MyEditor$getVerseNod;
const editor = this.slateEditor;
const selection = editor.selection;
if (!selection || !_MyEditor.MyEditor.isVerseOrChapterNumberSelected(editor)) {
return;
}
console.debug("selection before correction: ", selection);
const anchorVersePath = (_MyEditor$getVerseNod = _MyEditor.MyEditor.getVerseNode(editor, selection.anchor.path)) === null || _MyEditor$getVerseNod === void 0 ? void 0 : _MyEditor$getVerseNod[1];
if (_slate.Range.isCollapsed(selection) && anchorVersePath) {
// This can happen when nodes get merged after the user presses delete at the
// start of a verse. The solution is to move to the start of the inline container.
_SelectionTransforms.SelectionTransforms.moveToStartOfFirstLeaf(editor, anchorVersePath.concat(1));
} else if (_slate.Range.isBackward(selection)) {
// There is currently no solution to the problem when the user selects backwards
// through a verse number. Setting the focus to the start of the verse at which
// the selection began seems reasonable, but it does not consistently work.
_slate.Transforms.deselect(this.slateEditor);
} else if (_slate.Range.isForward(selection) && anchorVersePath) {
// When the user selects forwards through a verse number, we need to set
// the focus to the end of the verse at which they started the selection.
// If the errant selection was the result of a double/triple click, we can be assured
// that the user's selection came from the left (see the jsdoc for SelectionSeparator),
// so we take the same action here.
_SelectionTransforms.SelectionTransforms.moveToEndOfLastLeaf(editor, anchorVersePath, {
edge: "focus"
});
}
}
updateSelectedVerse(chapter, verseNumOrRange) {
const {
verseStart,
verseEnd
} = getVerseStartAndEnd(verseNumOrRange);
const newSelectedVerse = {
chapter: chapter,
verse: verseStart
};
this.setState({
selectedVerse: newSelectedVerse
});
if (!this.props.onVerseChange) return;
this.props.onVerseChange({
chapter,
verseStart,
verseEnd
});
}
didSelectedVerseChange(chapter, verseNumOrRange) {
const {
verseStart
} = getVerseStartAndEnd(verseNumOrRange);
const newSelectedVerse = {
chapter: chapter,
verse: verseStart
};
return !(0, _lodash.isEqual)(newSelectedVerse, this.state.selectedVerse);
}
componentDidMount() {
this.updateIdentificationFromUsfmAndProp();
if (this.props.goToVerse) {
this.goToVerse(this.props.goToVerse);
}
}
componentDidUpdate(prevProps) {
if (prevProps.usfmString != this.props.usfmString && prevProps.identification != this.props.identification) {
this.updateIdentificationFromUsfmAndProp();
} else if (prevProps.identification != this.props.identification) {
this.updateIdentificationFromProp();
} else if (prevProps.usfmString != this.props.usfmString) {
this.updateIdentificationFromUsfm();
}
if (prevProps.usfmString != this.props.usfmString) {
_slate.Transforms.deselect(this.slateEditor);
}
if (!(0, _lodash.isEqual)(prevProps.goToVerse, this.props.goToVerse)) {
this.goToVerse(this.props.goToVerse);
}
}
render() {
// The selection may be invalid if the slate value is updated by an external source,
// e.g. when the usfmString property just changed.
if (this.slateEditor.selection && isInvalidRange(this.slateEditor.selection, this.state.value)) {
_slate.Transforms.deselect(this.slateEditor);
}
return /*#__PURE__*/React.createElement(_slateReact.Slate, {
editor: this.slateEditor,
value: this.state.value,
onChange: this.handleChange
}, /*#__PURE__*/React.createElement(_slateReact.Editable, {
readOnly: this.props.readOnly,
renderElement: _usfmRenderer.renderElementByType,
renderLeaf: _usfmRenderer.renderLeafByProps,
spellCheck: false,
onKeyDown: this.onKeyDown,
className: "usfm-editor"
}));
}
}
exports.BasicUsfmEditor = BasicUsfmEditor;
_defineProperty(BasicUsfmEditor, "propTypes", _UsfmEditor.usfmEditorPropTypes);
_defineProperty(BasicUsfmEditor, "defaultProps", _UsfmEditor.usfmEditorDefaultProps);
function getVerseStartAndEnd(verseNumOrRange) {
if (verseNumOrRange == "front") return {
verseStart: 0,
verseEnd: 0
};
const [startVerseStr, endVerseStrOrNull] = verseNumOrRange.split("-");
const verseStart = parseInt(startVerseStr);
const verseEnd = parseInt(endVerseStrOrNull) || verseStart;
return {
verseStart,
verseEnd
};
}
function isInvalidRange(range, nodes) {
const anchorPath = range.anchor.path;
const focusPath = range.focus.path;
return nodes.length <= Math.min(anchorPath[0], focusPath[0]) || !_slate.Node.has(nodes[anchorPath[0]], anchorPath.slice(1)) || !_slate.Node.has(nodes[focusPath[0]], focusPath.slice(1));
}