UNPKG

@atlaskit/editor-plugin-code-block

Version:

Code block plugin for @atlaskit/editor-core

188 lines (173 loc) 9.82 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.validateWordWrappedDecorators = exports.updateDecorationSetWithWordWrappedDecorator = exports.updateDecorationSetWithLineNumberDecorators = exports.updateCodeBlockDecorations = exports.getWordWrapDecoratorsFromNodePos = exports.generateLineAttributesFromNode = exports.generateInitialDecorations = exports.createDecorationSetFromLineAttributes = exports.DECORATION_WRAPPED_BLOCK_NODE_TYPE = exports.DECORATION_WIDGET_TYPE = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _codeBlock = require("@atlaskit/editor-common/code-block"); var _view = require("@atlaskit/editor-prosemirror/view"); var _classNames = require("../ui/class-names"); var _utils = require("./utils"); var DECORATION_WIDGET_TYPE = exports.DECORATION_WIDGET_TYPE = 'decorationWidgetType'; var DECORATION_WRAPPED_BLOCK_NODE_TYPE = exports.DECORATION_WRAPPED_BLOCK_NODE_TYPE = 'decorationNodeType'; /** * Generate the initial decorations for the code block. */ var generateInitialDecorations = exports.generateInitialDecorations = function generateInitialDecorations(state) { var codeBlockNodes = (0, _utils.getAllCodeBlockNodesInDoc)(state); return codeBlockNodes.flatMap(function (node) { return createDecorationSetFromLineAttributes(generateLineAttributesFromNode(node)); }); }; /** * Update all the decorations used by the code block. */ var updateCodeBlockDecorations = exports.updateCodeBlockDecorations = function updateCodeBlockDecorations(tr, codeBlockNodes, decorationSet) { var updatedDecorationSet = decorationSet; // Update line number decorators for changed code block nodes if new line added or line removed. updatedDecorationSet = updateDecorationSetWithLineNumberDecorators(tr, codeBlockNodes, updatedDecorationSet); // Check to make sure the word wrap decorators are still valid. updatedDecorationSet = validateWordWrappedDecorators(tr, codeBlockNodes, updatedDecorationSet); return updatedDecorationSet; }; /** * Update the decorations set with the line number decorators. This will only happen for the code blocks passed to this function * when there has been a new line added or removed. The line decorations will not update the code block node otherwise. */ var updateDecorationSetWithLineNumberDecorators = exports.updateDecorationSetWithLineNumberDecorators = function updateDecorationSetWithLineNumberDecorators(tr, codeBlockNodes, decorationSet) { var updatedDecorationSet = decorationSet; var lineNumberDecorators = []; codeBlockNodes.forEach(function (node) { var existingWidgetsOnNode = updatedDecorationSet.find(node.pos, node.pos + node.node.nodeSize, function (spec) { return spec.type === DECORATION_WIDGET_TYPE; }); var newLineAttributes = generateLineAttributesFromNode(node); // There will be no widgets on initialisation. If the number of existing widgets does not equal the amount of lines, regenerate the widgets. // There may be a case where the number of existing widgets and the number of lines are the same, that's why we track totalLineCount. This allows // us to know how many lines there were when the widget was created. It avoids a break in line numbers, e.g. "1, 2, 3, 5, 6". Happens on line removal. if (existingWidgetsOnNode.length === 0 || existingWidgetsOnNode.length !== newLineAttributes.length || existingWidgetsOnNode[0].spec.totalLineCount !== newLineAttributes.length) { updatedDecorationSet = updatedDecorationSet.remove(existingWidgetsOnNode); lineNumberDecorators.push.apply(lineNumberDecorators, (0, _toConsumableArray2.default)(createDecorationSetFromLineAttributes(newLineAttributes))); } }); return updatedDecorationSet.add(tr.doc, [].concat(lineNumberDecorators)); }; var generateLineAttributesFromNode = exports.generateLineAttributesFromNode = function generateLineAttributesFromNode(node) { var innerNode = node.node, pos = node.pos; // Get content node var contentNode = innerNode.content; // Get node text content var lineAttributes = []; // Early exit if content size is 0 if (contentNode.size === 0) { return [{ lineStart: pos + 1, lineNumber: 1 }]; } contentNode.forEach(function (child) { var nodeTextContent = child.textContent; var nodeStartPos = pos; var lineStartIndex = nodeStartPos; // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed) var newLineAttributes = nodeTextContent.split('\n').map(function (line, index) { var lineLength = line.length; var lineStart = lineStartIndex + 1; var lineNumber = index + 1; // Include the newline character and increment to keep tabs on line position lineStartIndex += lineLength + 1; return { lineStart: lineStart, lineNumber: lineNumber }; }); lineAttributes = [].concat((0, _toConsumableArray2.default)(lineAttributes), (0, _toConsumableArray2.default)(newLineAttributes)); }); return lineAttributes; }; var createDecorationSetFromLineAttributes = exports.createDecorationSetFromLineAttributes = function createDecorationSetFromLineAttributes(lineAttributes) { var widgetDecorations = lineAttributes.map(function (lineAttribute) { var lineStart = lineAttribute.lineStart, lineNumber = lineAttribute.lineNumber; // Passing a function to create the widget rather than directly passing a widget, as per ProseMirror docs. var createLineNumberWidget = function createLineNumberWidget() { var widget = document.createElement('span'); widget.textContent = "".concat(lineNumber); widget.classList.add(_classNames.codeBlockClassNames.lineNumberWidget); return widget; }; // side -1 is used so the line numbers are the first thing to the left of the lines of code. // totalLineCount is used to know whether or not to update the line numbers when a new line is added or removed. return _view.Decoration.widget(lineStart, createLineNumberWidget, { type: DECORATION_WIDGET_TYPE, side: -1, totalLineCount: lineAttributes.length }); }); return widgetDecorations; }; /** * There are edge cases like when a user drags and drops a code block node where the decorator breaks and no longer reflects * the correct word wrap state. This function validates that the decorator and the state are in line, otherwise it will * retrigger the logic to apply the word wrap decorator. */ var validateWordWrappedDecorators = exports.validateWordWrappedDecorators = function validateWordWrappedDecorators(tr, codeBlockNodes, decorationSet) { var updatedDecorationSet = decorationSet; codeBlockNodes.forEach(function (node) { var isCodeBlockWrappedInState = (0, _codeBlock.isCodeBlockWordWrapEnabled)(node.node); var isCodeBlockWrappedByDecorator = getWordWrapDecoratorsFromNodePos(node.pos, decorationSet).length !== 0; if (isCodeBlockWrappedInState !== isCodeBlockWrappedByDecorator) { updatedDecorationSet = updateDecorationSetWithWordWrappedDecorator(decorationSet, tr, node); } }); return updatedDecorationSet; }; /** * Update the decoration set with the word wrap decorator. */ var updateDecorationSetWithWordWrappedDecorator = exports.updateDecorationSetWithWordWrappedDecorator = function updateDecorationSetWithWordWrappedDecorator(decorationSet, tr, node) { if (!node || node.pos === undefined) { return decorationSet; } var updatedDecorationSet = decorationSet; var pos = node.pos, innerNode = node.node; var isNodeWrapped = (0, _codeBlock.isCodeBlockWordWrapEnabled)(innerNode); if (!isNodeWrapped) { var currentWrappedBlockDecorationSet = getWordWrapDecoratorsFromNodePos(pos, updatedDecorationSet); updatedDecorationSet = updatedDecorationSet.remove(currentWrappedBlockDecorationSet); // In code block advanced we don't use the node decoration - however a change in decorations // is how we detect updates to the word wrap. If we have no decorations attached we've // likely changed the breakout width with the toggle ON - we add an empty decoration to force // prosemirror to recognise the change so we can update in our node view. This gets filtered // out on the next toggle. if (currentWrappedBlockDecorationSet.length === 0 && pos + innerNode.nodeSize <= tr.doc.content.size) { var wrappedBlock = _view.Decoration.node(pos, pos + innerNode.nodeSize, {}, { type: DECORATION_WRAPPED_BLOCK_NODE_TYPE } // Allows for quick filtering of decorations while using `find` ); updatedDecorationSet = updatedDecorationSet.add(tr.doc, [wrappedBlock]); } } else { var _wrappedBlock = _view.Decoration.node(pos, pos + innerNode.nodeSize, { class: _classNames.codeBlockClassNames.contentWrapped }, { type: DECORATION_WRAPPED_BLOCK_NODE_TYPE } // Allows for quick filtering of decorations while using `find` ); updatedDecorationSet = updatedDecorationSet.add(tr.doc, [_wrappedBlock]); } return updatedDecorationSet; }; /** * Get the word wrap decorators for the given node position. */ var getWordWrapDecoratorsFromNodePos = exports.getWordWrapDecoratorsFromNodePos = function getWordWrapDecoratorsFromNodePos(pos, decorationSet) { var codeBlockNodePosition = pos + 1; // We need to add 1 to the position to get the start of the node. var currentWrappedBlockDecorationSet = decorationSet.find(codeBlockNodePosition, codeBlockNodePosition, function (spec) { return spec.type === DECORATION_WRAPPED_BLOCK_NODE_TYPE; }); return currentWrappedBlockDecorationSet; };