UNPKG

@atlaskit/editor-plugin-code-block-advanced

Version:

CodeBlockAdvanced plugin for @atlaskit/editor-core

492 lines (484 loc) 24.8 kB
"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); }; };