@atlaskit/editor-plugin-code-block-advanced
Version:
CodeBlockAdvanced plugin for @atlaskit/editor-core
492 lines (484 loc) • 24.8 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getCodeBlockAdvancedNodeView = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _autocomplete = require("@codemirror/autocomplete");
var _language = require("@codemirror/language");
var _state = require("@codemirror/state");
var _view = require("@codemirror/view");
var _bindEventListener = require("bind-event-listener");
var _browser = require("@atlaskit/editor-common/browser");
var _codeBlock = require("@atlaskit/editor-common/code-block");
var _floatingToolbar = require("@atlaskit/editor-common/floating-toolbar");
var _messages = require("@atlaskit/editor-common/messages");
var _whitespace = require("@atlaskit/editor-common/whitespace");
var _state2 = require("@atlaskit/editor-prosemirror/state");
var _view2 = require("@atlaskit/editor-prosemirror/view");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
var _syntaxHighlightingTheme = require("../ui/syntaxHighlightingTheme");
var _theme = require("../ui/theme");
var _analytics = require("./analytics");
var _syncCMWithPM = require("./codemirrorSync/syncCMWithPM");
var _updateCMSelection = require("./codemirrorSync/updateCMSelection");
var _firstCodeBlockInDocument = require("./extensions/firstCodeBlockInDocument");
var _foldGutter = require("./extensions/foldGutter");
var _keymap = require("./extensions/keymap");
var _lineSeparator = require("./extensions/lineSeparator");
var _manageSelectionMarker = require("./extensions/manageSelectionMarker");
var _prosemirrorDecorations = require("./extensions/prosemirrorDecorations");
var _tripleClickExtension = require("./extensions/tripleClickExtension");
var _getLanguageName = _interopRequireDefault(require("./languages/getLanguageName"));
var _loader = require("./languages/loader");
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
// Based on: https://prosemirror.net/examples/codemirror/
var CodeBlockAdvancedNodeView = /*#__PURE__*/function () {
function CodeBlockAdvancedNodeView(node, view, getPos, innerDecorations, config) {
var _this = this,
_config$api,
_contentFormatSharedS,
_config$api2,
_config$api3,
_config$api4;
(0, _classCallCheck2.default)(this, CodeBlockAdvancedNodeView);
(0, _defineProperty2.default)(this, "lineWrappingCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "lineNumbersCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "languageCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "readOnlyCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "pmDecorationsCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "themeCompartment", new _state.Compartment());
(0, _defineProperty2.default)(this, "maybeTryingToReachNodeSelection", false);
(0, _defineProperty2.default)(this, "mouseDownInBorderArea", false);
(0, _defineProperty2.default)(this, "pmFacet", _state.Facet.define());
(0, _defineProperty2.default)(this, "wordWrappingEnabled", false);
(0, _defineProperty2.default)(this, "lineNumbersHidden", false);
(0, _defineProperty2.default)(this, "selectCodeBlockNodeAndFocus", function () {
_this.selectCodeBlockNode(undefined);
_this.view.focus();
});
(0, _defineProperty2.default)(this, "fireCodeFoldingAnalytics", function (folded, trigger) {
var _this$config$api;
if (!(0, _platformFeatureFlags.fg)('platform_editor_code_block_folding_analytics')) {
return;
}
(_this$config$api = _this.config.api) === null || _this$config$api === void 0 || (_this$config$api = _this$config$api.analytics) === null || _this$config$api === void 0 || _this$config$api.actions.fireAnalyticsEvent((0, _analytics.getCodeFoldingAnalyticsPayload)(folded, trigger));
});
(0, _defineProperty2.default)(this, "handleBorderAreaMouseDown", function (event) {
var isBorderArea = _this.isBorderAreaClick(event);
_this.mouseDownInBorderArea = isBorderArea;
if (isBorderArea && event.target === _this.cm.scrollDOM) {
// Stop CodeMirror from restoring stale inner selection before node selection.
event.preventDefault();
}
});
(0, _defineProperty2.default)(this, "handleBorderAreaClick", function (event) {
if (!_this.mouseDownInBorderArea) {
return;
}
// Prevent CodeMirror from restoring its inner selection after we hand focus and selection
// back to ProseMirror
event.preventDefault();
_this.maybeTryingToReachNodeSelection = true;
_this.view.focus();
_this.selectCodeBlockNode(undefined);
});
this.config = config;
this.node = node;
this.view = view;
this.getPos = getPos;
var contentFormatSharedState = (_config$api = config.api) === null || _config$api === void 0 || (_config$api = _config$api.contentFormat) === null || _config$api === void 0 ? void 0 : _config$api.sharedState;
this.contentMode = contentFormatSharedState === null || contentFormatSharedState === void 0 || (_contentFormatSharedS = contentFormatSharedState.currentState) === null || _contentFormatSharedS === void 0 || (_contentFormatSharedS = _contentFormatSharedS.call(contentFormatSharedState)) === null || _contentFormatSharedS === void 0 ? void 0 : _contentFormatSharedS.contentMode;
this.selectionAPI = (_config$api2 = config.api) === null || _config$api2 === void 0 || (_config$api2 = _config$api2.selection) === null || _config$api2 === void 0 ? void 0 : _config$api2.actions;
var getNode = function getNode() {
return _this.node;
};
var onMaybeNodeSelection = function onMaybeNodeSelection() {
return _this.maybeTryingToReachNodeSelection = true;
};
this.cleanupDisabledState = (_config$api3 = config.api) === null || _config$api3 === void 0 || (_config$api3 = _config$api3.editorDisabled) === null || _config$api3 === void 0 ? void 0 : _config$api3.sharedState.onChange(function () {
_this.updateReadonlyState();
});
this.languageLoader = new _loader.LanguageLoader(function (lang) {
_this.updating = true;
_this.cm.dispatch({
effects: _this.languageCompartment.reconfigure(lang)
});
_this.updating = false;
});
var _config$getIntl = config.getIntl(),
formatMessage = _config$getIntl.formatMessage;
var formattedAriaLabel = formatMessage(_messages.blockTypeMessages.codeblock);
var isMacOS = (0, _browser.getBrowserInfo)().mac;
this.cm = new _view.EditorView({
doc: this.node.textContent,
extensions: [].concat((0, _toConsumableArray2.default)(config.extensions), [this.lineWrappingCompartment.of((0, _codeBlock.isCodeBlockWordWrapEnabled)(node) ? _view.EditorView.lineWrapping : []), this.languageCompartment.of([]), this.pmDecorationsCompartment.of(this.pmFacet.compute([], function () {
return innerDecorations;
})), (0, _keymap.keymapExtension)({
view: view,
getPos: getPos,
getNode: getNode,
selectCodeBlockNode: this.selectCodeBlockNode.bind(this),
onMaybeNodeSelection: onMaybeNodeSelection,
customFindReplace: Boolean((_config$api4 = config.api) === null || _config$api4 === void 0 ? void 0 : _config$api4.findReplace)
}),
// Goes before cmTheme to override styles
config.allowCodeFolding ? [_theme.codeFoldingTheme] : [], this.themeCompartment.of((0, _theme.cmTheme)({
contentMode: this.contentMode
})), (0, _language.syntaxHighlighting)(_syntaxHighlightingTheme.highlightStyle), (0, _language.bracketMatching)(), (0, _expValEquals.expValEquals)('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? this.lineNumbersCompartment.of(this.getLineNumberVisibilityExtensions(node)) : this.getLineNumberExtensions(),
// Explicitly disable "sticky" positioning on all gutters to match
// Renderer behaviour.
(0, _view.gutters)({
fixed: false
}), _view.EditorView.updateListener.of(function (update) {
return _this.forwardUpdate(update);
}), this.readOnlyCompartment.of([_state.EditorState.readOnly.of(!this.view.editable), _view.EditorView.contentAttributes.of({
contentEditable: "".concat(this.view.editable)
})]), (0, _autocomplete.closeBrackets)(), _view.EditorView.editorAttributes.of(_objectSpread({
class: 'code-block'
}, (0, _platformFeatureFlags.fg)('platform_editor_adf_with_localid') && {
'data-local-id': this.node.attrs.localId
})), (0, _manageSelectionMarker.manageSelectionMarker)(config.api), (0, _prosemirrorDecorations.prosemirrorDecorationPlugin)(this.pmFacet, view, getPos), (0, _tripleClickExtension.tripleClickSelectAllExtension)(), (0, _firstCodeBlockInDocument.firstCodeBlockInDocument)(getPos), _view.EditorView.contentAttributes.of(_objectSpread(_objectSpread(_objectSpread({}, !(0, _expValEquals.expValEquals)('editor_a11y_role_textbox', 'isEnabled', true) && {
'aria-label': "".concat(formattedAriaLabel)
}), isMacOS && (0, _expValEquals.expValEquals)('editor_a11y_role_textbox', 'isEnabled', true) && {
role: 'textbox',
'aria-roledescription': formatMessage(_messages.roleDescriptionMessages.codeSnippetTextBox),
'aria-describedby': "codesnippet-".concat(this.node.attrs.localId),
'aria-multiline': 'true',
'aria-label': formattedAriaLabel
}), !isMacOS && (0, _expValEquals.expValEquals)('editor_a11y_role_textbox', 'isEnabled', true) && {
'aria-label': formattedAriaLabel,
'aria-describedby': "codesnippet-".concat(this.node.attrs.localId)
})), config.allowCodeFolding ? [(0, _foldGutter.foldGutterExtension)({
selectNode: this.selectCodeBlockNodeAndFocus,
getNode: function getNode() {
return _this.node;
},
onFoldToggled: this.fireCodeFoldingAnalytics
})] : [],
// With platform_editor_fix_advanced_codeblocks_crlf_patch the lineSeparatorExtension is not needed
(0, _expValEquals.expValEquals)('platform_editor_fix_advanced_codeblocks_crlf_patch', 'isEnabled', true) ? [] : [(0, _lineSeparator.lineSeparatorExtension)()]])
});
if (contentFormatSharedState) {
this.unsubscribeContentFormat = contentFormatSharedState.onChange(function (_ref) {
var nextSharedState = _ref.nextSharedState,
prevSharedState = _ref.prevSharedState;
var prevMode = prevSharedState === null || prevSharedState === void 0 ? void 0 : prevSharedState.contentMode;
var nextMode = nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.contentMode;
if (nextMode === prevMode) {
return;
}
_this.applyContentModeTheme(nextMode);
if (_this.updating || _this.cm.hasFocus) {
return;
}
_this.cm.requestMeasure();
});
}
if ((0, _expValEquals.expValEquals)('platform_editor_code_block_q4_lovability', 'isEnabled', true)) {
this.cleanupBorderAreaMouseDown = (0, _bindEventListener.bind)(this.cm.scrollDOM, {
type: 'mousedown',
listener: this.handleBorderAreaMouseDown
});
this.cleanupBorderAreaClick = (0, _bindEventListener.bind)(this.cm.scrollDOM, {
type: 'click',
listener: this.handleBorderAreaClick
});
}
// We append an additional element that fixes a selection bug on chrome if the code block
// is the first element followed by subsequent code blocks
var spaceContainer = document.createElement('span');
spaceContainer.innerText = _whitespace.ZERO_WIDTH_SPACE;
spaceContainer.style.height = '0';
// The editor's outer node is our DOM representation
this.dom = this.cm.dom;
this.dom.appendChild(spaceContainer);
if ((0, _expValEquals.expValEquals)('editor_a11y_role_textbox', 'isEnabled', true) && (0, _platformFeatureFlags.fg)('platform_editor_adf_with_localid')) {
this.invisibleAriaDescription = document.createElement('span');
this.invisibleAriaDescription.hidden = true;
this.invisibleAriaDescription.id = "codesnippet-".concat(this.node.attrs.localId);
this.updateAriaDescription();
this.dom.appendChild(this.invisibleAriaDescription);
}
// This flag is used to avoid an update loop between the outer and
// inner editor
this.updating = false;
this.updateLanguage();
this.updateLocalIdAttribute();
this.wordWrappingEnabled = (0, _codeBlock.isCodeBlockWordWrapEnabled)(node);
this.lineNumbersHidden = (0, _codeBlock.areCodeBlockLineNumbersHidden)(node);
// Restore fold state after initialization
if (config.allowCodeFolding) {
this.restoreFoldState();
}
}
return (0, _createClass2.default)(CodeBlockAdvancedNodeView, [{
key: "destroy",
value: function destroy() {
var _this$cleanupBorderAr, _this$cleanupBorderAr2, _this$cleanupDisabled, _this$unsubscribeCont;
// ED-27428: CodeMirror gets into an infinite loop as it detects mutations on removed
// decorations. When we change the breakout we destroy the node and cleanup these decorations from
// codemirror
this.clearProseMirrorDecorations();
(_this$cleanupBorderAr = this.cleanupBorderAreaMouseDown) === null || _this$cleanupBorderAr === void 0 || _this$cleanupBorderAr.call(this);
(_this$cleanupBorderAr2 = this.cleanupBorderAreaClick) === null || _this$cleanupBorderAr2 === void 0 || _this$cleanupBorderAr2.call(this);
(_this$cleanupDisabled = this.cleanupDisabledState) === null || _this$cleanupDisabled === void 0 || _this$cleanupDisabled.call(this);
(_this$unsubscribeCont = this.unsubscribeContentFormat) === null || _this$unsubscribeCont === void 0 || _this$unsubscribeCont.call(this);
}
}, {
key: "forwardUpdate",
value: function forwardUpdate(update) {
var _this$getPos, _this$getPos2;
if (this.updating || !this.cm.hasFocus) {
return;
}
var offset = ((_this$getPos = (_this$getPos2 = this.getPos) === null || _this$getPos2 === void 0 ? void 0 : _this$getPos2.call(this)) !== null && _this$getPos !== void 0 ? _this$getPos : 0) + 1;
(0, _syncCMWithPM.syncCMWithPM)({
view: this.view,
update: update,
offset: offset
});
}
}, {
key: "setSelection",
value: function setSelection(anchor, head) {
if (!this.maybeTryingToReachNodeSelection) {
this.cm.focus();
}
this.updating = true;
this.cm.dispatch({
selection: {
anchor: anchor,
head: head
}
});
this.updating = false;
}
}, {
key: "updateReadonlyState",
value: function updateReadonlyState() {
this.updating = true;
this.cm.dispatch({
effects: this.readOnlyCompartment.reconfigure([_state.EditorState.readOnly.of(!this.view.editable), _view.EditorView.contentAttributes.of({
contentEditable: "".concat(this.view.editable)
})])
});
this.updating = false;
}
}, {
key: "updateLanguage",
value: function updateLanguage() {
this.languageLoader.updateLanguage(this.node.attrs.language);
if ((0, _expValEquals.expValEquals)('editor_a11y_role_textbox', 'isEnabled', true) && (0, _platformFeatureFlags.fg)('platform_editor_adf_with_localid')) {
this.updateAriaDescription();
}
}
}, {
key: "updateAriaDescription",
value: function updateAriaDescription() {
if (!this.invisibleAriaDescription) {
return;
}
var _this$config$getIntl = this.config.getIntl(),
formatMessage = _this$config$getIntl.formatMessage;
var languageName = (0, _getLanguageName.default)(this.node.attrs.language);
if (languageName) {
this.invisibleAriaDescription.textContent = "".concat(formatMessage(_messages.codeBlockMessages.codeblockLanguageAriaDescription, {
language: languageName
}), " ").concat(formatMessage(_floatingToolbar.messages.floatingToolbarAnnouncer));
} else {
// If the lanuage is undefined provide a more human readable message
this.invisibleAriaDescription.textContent = "".concat(formatMessage(_messages.codeBlockMessages.codeBlockLanguageNotSet), " ").concat(formatMessage(_floatingToolbar.messages.floatingToolbarAnnouncer));
}
}
}, {
key: "updateLocalIdAttribute",
value: function updateLocalIdAttribute() {
if ((0, _platformFeatureFlags.fg)('platform_editor_adf_with_localid')) {
var localId = this.node.attrs.localId;
if (localId) {
this.cm.dom.setAttribute('data-local-id', localId);
} else {
this.cm.dom.removeAttribute('data-local-id');
}
}
}
}, {
key: "selectCodeBlockNode",
value: function selectCodeBlockNode(relativeSelectionPos) {
var _this$selectionAPI, _this$getPos3, _this$getPos4;
var tr = (_this$selectionAPI = this.selectionAPI) === null || _this$selectionAPI === void 0 ? void 0 : _this$selectionAPI.selectNearNode({
selectionRelativeToNode: relativeSelectionPos,
selection: _state2.NodeSelection.create(this.view.state.doc, (_this$getPos3 = (_this$getPos4 = this.getPos) === null || _this$getPos4 === void 0 ? void 0 : _this$getPos4.call(this)) !== null && _this$getPos3 !== void 0 ? _this$getPos3 : 0)
})(this.view.state);
if (tr) {
this.view.dispatch(tr);
}
}
}, {
key: "getLineNumberExtensions",
value: function getLineNumberExtensions() {
var _this2 = this;
return [(0, _view.lineNumbers)({
domEventHandlers: {
click: function click() {
_this2.selectCodeBlockNodeAndFocus();
return true;
}
}
})];
}
}, {
key: "getLineNumberVisibilityExtensions",
value: function getLineNumberVisibilityExtensions(node) {
if ((0, _codeBlock.areCodeBlockLineNumbersHidden)(node)) {
return [];
}
return this.getLineNumberExtensions();
}
}, {
key: "getLineNumbersEffects",
value: function getLineNumbersEffects(node) {
var lineNumbersHidden = (0, _codeBlock.areCodeBlockLineNumbersHidden)(node);
if (this.lineNumbersHidden !== lineNumbersHidden) {
this.lineNumbersHidden = lineNumbersHidden;
return this.lineNumbersCompartment.reconfigure(this.getLineNumberVisibilityExtensions(node));
}
return undefined;
}
}, {
key: "getWordWrapEffects",
value: function getWordWrapEffects(node) {
if (this.wordWrappingEnabled !== (0, _codeBlock.isCodeBlockWordWrapEnabled)(node)) {
this.wordWrappingEnabled = !this.wordWrappingEnabled;
return this.lineWrappingCompartment.reconfigure((0, _codeBlock.isCodeBlockWordWrapEnabled)(node) ? _view.EditorView.lineWrapping : []);
}
return undefined;
}
}, {
key: "restoreFoldState",
value: function restoreFoldState() {
this.updating = true;
var effects = (0, _foldGutter.getCodeBlockFoldStateEffects)({
node: this.node,
cm: this.cm
});
if (effects) {
this.cm.dispatch({
effects: effects
});
}
this.updating = false;
}
}, {
key: "applyContentModeTheme",
value: function applyContentModeTheme(contentMode) {
if (contentMode === this.contentMode) {
return;
}
this.contentMode = contentMode;
this.updating = true;
this.cm.dispatch({
effects: this.themeCompartment.reconfigure((0, _theme.cmTheme)({
contentMode: contentMode
}))
});
this.updating = false;
}
}, {
key: "update",
value: function update(node, _, innerDecorations) {
this.maybeTryingToReachNodeSelection = false;
if (node.type !== this.node.type) {
return false;
}
this.node = node;
if (this.updating) {
return true;
}
this.updateLanguage();
this.updateLocalIdAttribute();
var newText = node.textContent,
curText = this.cm.state.doc.toString();
// Updates bundled for performance (to avoid multiple-dispatches)
var changes = (0, _updateCMSelection.getCMSelectionChanges)(curText, newText);
var wordWrapEffect = this.getWordWrapEffects(node);
var lineNumbersEffect = (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? this.getLineNumbersEffects(node) : undefined;
var prosemirrorDecorationsEffect = this.getProseMirrorDecorationEffects(innerDecorations);
if (changes || wordWrapEffect || lineNumbersEffect || prosemirrorDecorationsEffect) {
this.updating = true;
this.cm.dispatch({
effects: [wordWrapEffect, lineNumbersEffect, prosemirrorDecorationsEffect].filter(function (effect) {
return !!effect;
}),
changes: changes
});
this.updating = false;
}
return true;
}
/**
* Updates a facet which stores information on the prosemirror decorations
*
* This then gets translated to codemirror decorations in `prosemirrorDecorationPlugin`
* @param decorationSource
* @example
*/
}, {
key: "getProseMirrorDecorationEffects",
value: function getProseMirrorDecorationEffects(decorationSource) {
var computedFacet = this.pmFacet.compute([], function () {
return decorationSource;
});
return this.pmDecorationsCompartment.reconfigure(computedFacet);
}
}, {
key: "clearProseMirrorDecorations",
value: function clearProseMirrorDecorations() {
this.updating = true;
var computedFacet = this.pmFacet.compute([], function () {
return _view2.DecorationSet.empty;
});
this.cm.dispatch({
effects: this.pmDecorationsCompartment.reconfigure(computedFacet)
});
this.updating = false;
}
}, {
key: "isBorderAreaClick",
value: function isBorderAreaClick(event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
return false;
}
// CodeMirror does not expose a stable gutter DOM handle, so this relies on its
// class names to distinguish interactive content/gutters from border-area clicks.
return !target.closest('.cm-gutters') && !target.closest('.cm-line');
}
}, {
key: "stopEvent",
value: function stopEvent(e) {
var _this$getPos5;
// If we have selected the node we should not stop these events
if ((e instanceof KeyboardEvent || e instanceof ClipboardEvent) && this.view.state.selection instanceof _state2.NodeSelection && this.view.state.selection.from === ((_this$getPos5 = this.getPos) === null || _this$getPos5 === void 0 ? void 0 : _this$getPos5.call(this))) {
return false;
}
if (e instanceof DragEvent && e.type === 'dragenter' && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_controls_perf_optimization', 'isEnabled', true)) {
return false; // Allow dragenter to propagate so that the editor can handle it
}
return true;
}
}]);
}();
var getCodeBlockAdvancedNodeView = exports.getCodeBlockAdvancedNodeView = function getCodeBlockAdvancedNodeView(props) {
return function (node, view, getPos, innerDecorations) {
return new CodeBlockAdvancedNodeView(node, view, getPos, innerDecorations, props);
};
};