UNPKG

@atlaskit/editor-plugin-code-block

Version:

Code block plugin for @atlaskit/editor-core

181 lines (167 loc) 8.75 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { isCodeBlockWordWrapEnabled } from '@atlaskit/editor-common/code-block'; import { Decoration } from '@atlaskit/editor-prosemirror/view'; import { codeBlockClassNames } from '../ui/class-names'; import { getAllCodeBlockNodesInDoc } from './utils'; export var DECORATION_WIDGET_TYPE = 'decorationWidgetType'; export var DECORATION_WRAPPED_BLOCK_NODE_TYPE = 'decorationNodeType'; /** * Generate the initial decorations for the code block. */ export var generateInitialDecorations = function generateInitialDecorations(state) { var codeBlockNodes = getAllCodeBlockNodesInDoc(state); return codeBlockNodes.flatMap(function (node) { return createDecorationSetFromLineAttributes(generateLineAttributesFromNode(node)); }); }; /** * Update all the decorations used by the code block. */ export var 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. */ export var 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, _toConsumableArray(createDecorationSetFromLineAttributes(newLineAttributes))); } }); return updatedDecorationSet.add(tr.doc, [].concat(lineNumberDecorators)); }; export var 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(_toConsumableArray(lineAttributes), _toConsumableArray(newLineAttributes)); }); return lineAttributes; }; export var 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(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 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. */ export var validateWordWrappedDecorators = function validateWordWrappedDecorators(tr, codeBlockNodes, decorationSet) { var updatedDecorationSet = decorationSet; codeBlockNodes.forEach(function (node) { var isCodeBlockWrappedInState = 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. */ export var 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 = 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 = 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 = Decoration.node(pos, pos + innerNode.nodeSize, { class: 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. */ export var 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; };