@atlaskit/editor-plugin-code-block
Version:
Code block plugin for @atlaskit/editor-core
188 lines (173 loc) • 9.82 kB
JavaScript
"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;
};