@atlaskit/editor-plugin-code-block 12.1.6 → 12.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/editor-commands/index.js +5 -3
  3. package/dist/cjs/pm-plugins/toolbar.js +23 -24
  4. package/dist/cjs/ui/CodeBlockLanguagePicker.js +45 -0
  5. package/dist/cjs/ui/LanguagePicker.js +63 -10
  6. package/dist/cjs/ui/language-picker-options.js +21 -4
  7. package/dist/cjs/ui/recent-languages.js +50 -0
  8. package/dist/es2019/editor-commands/index.js +5 -2
  9. package/dist/es2019/pm-plugins/toolbar.js +168 -167
  10. package/dist/es2019/ui/CodeBlockLanguagePicker.js +31 -0
  11. package/dist/es2019/ui/LanguagePicker.js +41 -9
  12. package/dist/es2019/ui/language-picker-options.js +15 -4
  13. package/dist/es2019/ui/recent-languages.js +38 -0
  14. package/dist/esm/editor-commands/index.js +5 -3
  15. package/dist/esm/pm-plugins/toolbar.js +24 -25
  16. package/dist/esm/ui/CodeBlockLanguagePicker.js +36 -0
  17. package/dist/esm/ui/LanguagePicker.js +64 -11
  18. package/dist/esm/ui/language-picker-options.js +21 -4
  19. package/dist/esm/ui/recent-languages.js +43 -0
  20. package/dist/types/editor-commands/index.d.ts +2 -1
  21. package/dist/types/ui/CodeBlockLanguagePicker.d.ts +5 -0
  22. package/dist/types/ui/LanguagePicker.d.ts +7 -5
  23. package/dist/types/ui/language-picker-options.d.ts +2 -0
  24. package/dist/types/ui/recent-languages.d.ts +4 -0
  25. package/dist/types-ts4.5/editor-commands/index.d.ts +2 -1
  26. package/dist/types-ts4.5/ui/CodeBlockLanguagePicker.d.ts +5 -0
  27. package/dist/types-ts4.5/ui/LanguagePicker.d.ts +7 -5
  28. package/dist/types-ts4.5/ui/language-picker-options.d.ts +2 -0
  29. package/dist/types-ts4.5/ui/recent-languages.d.ts +4 -0
  30. package/package.json +5 -4
@@ -10,189 +10,190 @@ import TextWrapIcon from '@atlaskit/icon/core/text-wrap';
10
10
  import { fg } from '@atlaskit/platform-feature-flags';
11
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
12
  import { changeLanguage, copyContentToClipboardWithAnalytics, removeCodeBlockWithAnalytics, resetCopiedState, toggleLineNumbersForCodeBlockNode, toggleWordWrapStateForCodeBlockNode } from '../editor-commands';
13
+ import { CodeBlockLanguagePicker } from '../ui/CodeBlockLanguagePicker';
13
14
  import { WrapIcon } from '../ui/icons/WrapIcon';
14
- import { createGroupedLanguageOptions, NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE } from '../ui/language-picker-options';
15
- import { LanguagePicker } from '../ui/LanguagePicker';
15
+ import { NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE } from '../ui/language-picker-options';
16
16
  import { provideVisualFeedbackForCopyButton, removeVisualFeedbackForCopyButton } from './codeBlockCopySelectionPlugin';
17
17
  import { createLanguageList, DEFAULT_LANGUAGES, getLanguageIdentifier } from './language-list';
18
18
  import { pluginKey } from './plugin-key';
19
- export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLanguageName = undefined) => (state, {
20
- formatMessage
21
- }) => {
22
- var _api$editorViewMode, _api$editorViewMode$s, _api$decorations$acti, _api$decorations, _api$analytics, _codeBlockState$pos, _node$attrs, _languagePicker;
23
- const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
24
- const {
25
- hoverDecoration
26
- } = (_api$decorations$acti = api === null || api === void 0 ? void 0 : (_api$decorations = api.decorations) === null || _api$decorations === void 0 ? void 0 : _api$decorations.actions) !== null && _api$decorations$acti !== void 0 ? _api$decorations$acti : {};
27
- const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
28
- const codeBlockState = pluginKey.getState(state);
29
- const pos = (_codeBlockState$pos = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.pos) !== null && _codeBlockState$pos !== void 0 ? _codeBlockState$pos : null;
30
- if (!codeBlockState || pos === null) {
31
- return;
32
- }
33
- const node = state.doc.nodeAt(pos);
34
- const nodeType = state.schema.nodes.codeBlock;
35
- if ((node === null || node === void 0 ? void 0 : node.type) !== nodeType) {
36
- return;
37
- }
38
- const isWrapped = isCodeBlockWordWrapEnabled(node);
39
- const areLineNumbersVisible = areCodeBlockLineNumbersVisible(node);
40
- const language = node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.language;
41
- const languageList = createLanguageList(overrideLanguageName ? DEFAULT_LANGUAGES.map(language => ({
42
- ...language,
43
- name: overrideLanguageName(language.name)
19
+ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLanguageName = undefined) => {
20
+ const languageList = createLanguageList(overrideLanguageName ? DEFAULT_LANGUAGES.map(languageOption => ({
21
+ ...languageOption,
22
+ name: overrideLanguageName(languageOption.name)
44
23
  })) : DEFAULT_LANGUAGES);
45
- const options = languageList.map(lang => ({
24
+ const languagePickerOptions = languageList.map(lang => ({
46
25
  label: lang.name,
47
26
  value: getLanguageIdentifier(lang),
48
27
  alias: lang.alias
49
28
  }));
50
-
51
- // If language is not undefined search for it in the value and then search in the aliases
52
- const defaultValue = language ? options.find(option => option.value === language) || options.find(option => option.alias.includes(language)) : null;
53
- const languageSelect = {
54
- id: 'editor.codeBlock.languageOptions',
55
- type: 'select',
56
- selectType: 'list',
57
- onChange: option => changeLanguage(editorAnalyticsAPI)(option.value),
58
- defaultValue,
59
- placeholder: formatMessage(codeBlockButtonMessages.selectLanguage),
60
- options,
61
- filterOption: languageListFilter
62
- };
63
- let languagePicker;
64
- if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button')) {
65
- const languagePickerOptions = options;
66
- const groupedOptions = createGroupedLanguageOptions({
67
- formatMessage,
68
- languages: languagePickerOptions
69
- });
70
- const defaultPickerValue = language ? languagePickerOptions.find(option => language === NONE_LANGUAGE_VALUE ? option.value === PLAIN_TEXT_LANGUAGE_VALUE : option.value === language || option.alias.includes(language)) : undefined;
71
- languagePicker = {
72
- type: 'custom',
73
- fallback: [],
74
- render: view => {
75
- if (!view) {
76
- return null;
29
+ return (state, {
30
+ formatMessage
31
+ }) => {
32
+ var _api$editorViewMode, _api$editorViewMode$s, _api$decorations$acti, _api$decorations, _api$analytics, _codeBlockState$pos, _node$attrs, _languagePicker;
33
+ const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
34
+ const {
35
+ hoverDecoration
36
+ } = (_api$decorations$acti = api === null || api === void 0 ? void 0 : (_api$decorations = api.decorations) === null || _api$decorations === void 0 ? void 0 : _api$decorations.actions) !== null && _api$decorations$acti !== void 0 ? _api$decorations$acti : {};
37
+ const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
38
+ const codeBlockState = pluginKey.getState(state);
39
+ const pos = (_codeBlockState$pos = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.pos) !== null && _codeBlockState$pos !== void 0 ? _codeBlockState$pos : null;
40
+ if (!codeBlockState || pos === null) {
41
+ return;
42
+ }
43
+ const node = state.doc.nodeAt(pos);
44
+ const nodeType = state.schema.nodes.codeBlock;
45
+ if ((node === null || node === void 0 ? void 0 : node.type) !== nodeType) {
46
+ return;
47
+ }
48
+ const isWrapped = isCodeBlockWordWrapEnabled(node);
49
+ const areLineNumbersVisible = areCodeBlockLineNumbersVisible(node);
50
+ const language = node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.language;
51
+ // Keep fresh option objects for the legacy toolbar select so reopening it
52
+ // continues to start from the top rather than preserving the previously
53
+ // focused option by reference.
54
+ const languageSelectOptions = languagePickerOptions.map(option => ({
55
+ ...option
56
+ }));
57
+ const defaultValue = language ? languageSelectOptions.find(option => option.value === language) || languageSelectOptions.find(option => option.alias.includes(language)) : null;
58
+ const languageSelect = {
59
+ id: 'editor.codeBlock.languageOptions',
60
+ type: 'select',
61
+ selectType: 'list',
62
+ onChange: option => changeLanguage(editorAnalyticsAPI)(option.value),
63
+ defaultValue,
64
+ placeholder: formatMessage(codeBlockButtonMessages.selectLanguage),
65
+ options: languageSelectOptions,
66
+ filterOption: languageListFilter
67
+ };
68
+ let languagePicker;
69
+ if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button')) {
70
+ const defaultPickerValue = language ? languagePickerOptions.find(option => language === NONE_LANGUAGE_VALUE ? option.value === PLAIN_TEXT_LANGUAGE_VALUE : option.value === language || option.alias.includes(language)) : undefined;
71
+ languagePicker = {
72
+ type: 'custom',
73
+ fallback: [],
74
+ render: view => {
75
+ if (!view) {
76
+ return null;
77
+ }
78
+ return /*#__PURE__*/React.createElement(CodeBlockLanguagePicker, {
79
+ api: api,
80
+ defaultValue: defaultPickerValue,
81
+ editorView: view,
82
+ filterOption: languageListFilter,
83
+ formatMessage: formatMessage,
84
+ languagePickerOptions: languagePickerOptions
85
+ });
77
86
  }
78
- return /*#__PURE__*/React.createElement(LanguagePicker, {
79
- api: api,
80
- defaultValue: defaultPickerValue,
81
- editorView: view,
82
- filterOption: languageListFilter,
83
- formatMessage: formatMessage,
84
- options: groupedOptions
85
- });
86
- }
87
+ };
88
+ }
89
+ const separator = {
90
+ type: 'separator'
87
91
  };
88
- }
89
- const separator = {
90
- type: 'separator'
91
- };
92
- const areAnyNewToolbarFlagsEnabled = areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar));
93
- const copyToClipboardItems = !allowCopyToClipboard ? [] : [{
94
- id: 'editor.codeBlock.copy',
95
- type: 'button',
96
- supportsViewMode: true,
97
- appearance: 'subtle',
98
- icon: CopyIcon,
99
- // note: copyContentToClipboardWithAnalytics contains logic that also removes the
100
- // visual feedback for the copy button
101
- onClick: copyContentToClipboardWithAnalytics(editorAnalyticsAPI),
102
- title: formatMessage(codeBlockState.contentCopied ? codeBlockButtonMessages.copiedCodeToClipboard : codeBlockButtonMessages.copyCodeToClipboard),
103
- onMouseEnter: provideVisualFeedbackForCopyButton,
104
- // note: resetCopiedState contains logic that also removes the
105
- // visual feedback for the copy button
106
- onMouseLeave: resetCopiedState,
107
- onFocus: provideVisualFeedbackForCopyButton,
108
- onBlur: removeVisualFeedbackForCopyButton,
109
- hideTooltipOnClick: false,
110
- disabled: codeBlockState.isNodeSelected,
111
- tabIndex: null
112
- }, separator];
113
- let copyAndDeleteButtonMenuItems = [];
114
- if (areAnyNewToolbarFlagsEnabled) {
115
- const overflowMenuOptions = [{
116
- title: formatMessage(commonMessages.delete),
117
- icon: DeleteIcon({
118
- label: ''
119
- }),
120
- onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
121
- onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
122
- onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
123
- onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
124
- onClick: removeCodeBlockWithAnalytics(editorAnalyticsAPI)
125
- }];
126
- if (allowCopyToClipboard) {
127
- overflowMenuOptions.unshift({
128
- title: formatMessage(commonMessages.copyToClipboard),
129
- onClick: copyContentToClipboardWithAnalytics(editorAnalyticsAPI),
130
- icon: CopyIcon({
92
+ const areAnyNewToolbarFlagsEnabled = areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar));
93
+ const copyToClipboardItems = !allowCopyToClipboard ? [] : [{
94
+ id: 'editor.codeBlock.copy',
95
+ type: 'button',
96
+ supportsViewMode: true,
97
+ appearance: 'subtle',
98
+ icon: CopyIcon,
99
+ // note: copyContentToClipboardWithAnalytics contains logic that also removes the
100
+ // visual feedback for the copy button
101
+ onClick: copyContentToClipboardWithAnalytics(editorAnalyticsAPI),
102
+ title: formatMessage(codeBlockState.contentCopied ? codeBlockButtonMessages.copiedCodeToClipboard : codeBlockButtonMessages.copyCodeToClipboard),
103
+ onMouseEnter: provideVisualFeedbackForCopyButton,
104
+ // note: resetCopiedState contains logic that also removes the
105
+ // visual feedback for the copy button
106
+ onMouseLeave: resetCopiedState,
107
+ onFocus: provideVisualFeedbackForCopyButton,
108
+ onBlur: removeVisualFeedbackForCopyButton,
109
+ hideTooltipOnClick: false,
110
+ disabled: codeBlockState.isNodeSelected,
111
+ tabIndex: null
112
+ }, separator];
113
+ let copyAndDeleteButtonMenuItems = [];
114
+ if (areAnyNewToolbarFlagsEnabled) {
115
+ const overflowMenuOptions = [{
116
+ title: formatMessage(commonMessages.delete),
117
+ icon: DeleteIcon({
131
118
  label: ''
132
119
  }),
133
- onMouseEnter: provideVisualFeedbackForCopyButton,
134
- onMouseLeave: resetCopiedState,
135
- onFocus: provideVisualFeedbackForCopyButton,
136
- onBlur: removeVisualFeedbackForCopyButton,
137
- disabled: codeBlockState.isNodeSelected
138
- });
120
+ onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
121
+ onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
122
+ onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
123
+ onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
124
+ onClick: removeCodeBlockWithAnalytics(editorAnalyticsAPI)
125
+ }];
126
+ if (allowCopyToClipboard) {
127
+ overflowMenuOptions.unshift({
128
+ title: formatMessage(commonMessages.copyToClipboard),
129
+ onClick: copyContentToClipboardWithAnalytics(editorAnalyticsAPI),
130
+ icon: CopyIcon({
131
+ label: ''
132
+ }),
133
+ onMouseEnter: provideVisualFeedbackForCopyButton,
134
+ onMouseLeave: resetCopiedState,
135
+ onFocus: provideVisualFeedbackForCopyButton,
136
+ onBlur: removeVisualFeedbackForCopyButton,
137
+ disabled: codeBlockState.isNodeSelected
138
+ });
139
+ }
140
+ copyAndDeleteButtonMenuItems = isViewMode ? [...copyToClipboardItems] : [{
141
+ type: 'separator',
142
+ fullHeight: true
143
+ }, {
144
+ type: 'overflow-dropdown',
145
+ testId: 'code-block-overflow-dropdown-trigger',
146
+ options: overflowMenuOptions
147
+ }];
148
+ } else {
149
+ const deleteButton = {
150
+ id: 'editor.codeBlock.delete',
151
+ type: 'button',
152
+ appearance: 'danger',
153
+ icon: DeleteIcon,
154
+ iconFallback: DeleteIcon,
155
+ onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
156
+ onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
157
+ onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
158
+ onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
159
+ onClick: removeCodeBlockWithAnalytics(editorAnalyticsAPI),
160
+ title: formatMessage(commonMessages.remove),
161
+ tabIndex: null
162
+ };
163
+ copyAndDeleteButtonMenuItems = [separator, ...copyToClipboardItems, deleteButton];
139
164
  }
140
- copyAndDeleteButtonMenuItems = isViewMode ? [...copyToClipboardItems] : [{
141
- type: 'separator',
142
- fullHeight: true
143
- }, {
144
- type: 'overflow-dropdown',
145
- testId: 'code-block-overflow-dropdown-trigger',
146
- options: overflowMenuOptions
147
- }];
148
- } else {
149
- const deleteButton = {
150
- id: 'editor.codeBlock.delete',
165
+ const codeBlockWrapButtonTitle = expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? formatMessage(isWrapped ? codeBlockButtonMessages.unwrapCodeLabel : codeBlockButtonMessages.wrapCodeLabel) : formatMessage(codeBlockButtonMessages.wrapCode);
166
+ const codeBlockWrapButton = {
167
+ id: 'editor.codeBlock.wrap',
151
168
  type: 'button',
152
- appearance: 'danger',
153
- icon: DeleteIcon,
154
- iconFallback: DeleteIcon,
155
- onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
156
- onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
157
- onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
158
- onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
159
- onClick: removeCodeBlockWithAnalytics(editorAnalyticsAPI),
160
- title: formatMessage(commonMessages.remove),
161
- tabIndex: null
169
+ // Toggling button now writes to ADF, hence it should be available in view mode
170
+ supportsViewMode: !expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true),
171
+ icon: TextWrapIcon,
172
+ iconFallback: WrapIcon,
173
+ onClick: toggleWordWrapStateForCodeBlockNode(editorAnalyticsAPI),
174
+ title: codeBlockWrapButtonTitle,
175
+ tabIndex: null,
176
+ selected: isWrapped
177
+ };
178
+ const codeBlockLineNumbersButton = {
179
+ id: 'editor.codeBlock.lineNumbers',
180
+ type: 'button',
181
+ supportsViewMode: false,
182
+ icon: ListNumberedIcon,
183
+ onClick: toggleLineNumbersForCodeBlockNode(editorAnalyticsAPI),
184
+ title: formatMessage(areLineNumbersVisible ? codeBlockButtonMessages.hideLineNumbersLabel : codeBlockButtonMessages.showLineNumbersLabel),
185
+ tabIndex: null,
186
+ selected: areLineNumbersVisible
187
+ };
188
+ return {
189
+ title: 'CodeBlock floating controls',
190
+ // Ignored via go/ees005
191
+ // eslint-disable-next-line @atlaskit/editor/no-as-casting
192
+ getDomRef: view => findDomRefAtPos(pos, view.domAtPos.bind(view)),
193
+ nodeType,
194
+ items: [(_languagePicker = languagePicker) !== null && _languagePicker !== void 0 ? _languagePicker : languageSelect, ...(areAnyNewToolbarFlagsEnabled ? [] : [separator]), codeBlockWrapButton, ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button') ? [codeBlockLineNumbersButton] : []), ...copyAndDeleteButtonMenuItems],
195
+ scrollable: true
162
196
  };
163
- copyAndDeleteButtonMenuItems = [separator, ...copyToClipboardItems, deleteButton];
164
- }
165
- const codeBlockWrapButtonTitle = expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? formatMessage(isWrapped ? codeBlockButtonMessages.unwrapCodeLabel : codeBlockButtonMessages.wrapCodeLabel) : formatMessage(codeBlockButtonMessages.wrapCode);
166
- const codeBlockWrapButton = {
167
- id: 'editor.codeBlock.wrap',
168
- type: 'button',
169
- // Toggling button now writes to ADF, hence it should be available in view mode
170
- supportsViewMode: !expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true),
171
- icon: TextWrapIcon,
172
- iconFallback: WrapIcon,
173
- onClick: toggleWordWrapStateForCodeBlockNode(editorAnalyticsAPI),
174
- title: codeBlockWrapButtonTitle,
175
- tabIndex: null,
176
- selected: isWrapped
177
- };
178
- const codeBlockLineNumbersButton = {
179
- id: 'editor.codeBlock.lineNumbers',
180
- type: 'button',
181
- supportsViewMode: false,
182
- icon: ListNumberedIcon,
183
- onClick: toggleLineNumbersForCodeBlockNode(editorAnalyticsAPI),
184
- title: formatMessage(areLineNumbersVisible ? codeBlockButtonMessages.hideLineNumbersLabel : codeBlockButtonMessages.showLineNumbersLabel),
185
- tabIndex: null,
186
- selected: areLineNumbersVisible
187
- };
188
- return {
189
- title: 'CodeBlock floating controls',
190
- // Ignored via go/ees005
191
- // eslint-disable-next-line @atlaskit/editor/no-as-casting
192
- getDomRef: view => findDomRefAtPos(pos, view.domAtPos.bind(view)),
193
- nodeType,
194
- items: [(_languagePicker = languagePicker) !== null && _languagePicker !== void 0 ? _languagePicker : languageSelect, ...(areAnyNewToolbarFlagsEnabled ? [] : [separator]), codeBlockWrapButton, ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button') ? [codeBlockLineNumbersButton] : []), ...copyAndDeleteButtonMenuItems],
195
- scrollable: true
196
197
  };
197
198
  };
198
199
 
@@ -0,0 +1,31 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { LanguagePicker } from './LanguagePicker';
3
+ import { getRecentLanguages, saveRecentLanguage } from './recent-languages';
4
+ export const CodeBlockLanguagePicker = ({
5
+ api,
6
+ defaultValue,
7
+ editorView,
8
+ filterOption,
9
+ formatMessage,
10
+ languagePickerOptions
11
+ }) => {
12
+ const [recentLanguageValues, setRecentLanguageValues] = useState(() => getRecentLanguages());
13
+ const refreshRecentLanguages = useCallback(() => {
14
+ setRecentLanguageValues(getRecentLanguages());
15
+ }, []);
16
+ const handleLanguageSelect = useCallback(language => {
17
+ saveRecentLanguage(language);
18
+ setRecentLanguageValues(getRecentLanguages());
19
+ }, []);
20
+ return /*#__PURE__*/React.createElement(LanguagePicker, {
21
+ api: api,
22
+ defaultValue: defaultValue,
23
+ editorView: editorView,
24
+ filterOption: filterOption,
25
+ formatMessage: formatMessage,
26
+ languagePickerOptions: languagePickerOptions,
27
+ recentLanguageValues: recentLanguageValues,
28
+ onLanguageSelect: handleLanguageSelect,
29
+ onMenuOpen: refreshRecentLanguages
30
+ });
31
+ };
@@ -1,7 +1,7 @@
1
1
  /* LanguagePicker.tsx generated by @compiled/babel-plugin v0.39.1 */
2
2
  import "./LanguagePicker.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { Fragment, useCallback, useMemo, useState } from 'react';
4
+ import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
5
5
  import Button from '@atlaskit/button/new';
6
6
  import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
7
7
  import { akEditorLineHeight } from '@atlaskit/editor-shared-styles';
@@ -9,6 +9,7 @@ import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
9
9
  import { Box } from '@atlaskit/primitives/compiled';
10
10
  import { PopupSelect, components } from '@atlaskit/select';
11
11
  import { changeLanguage } from '../editor-commands';
12
+ import { createGroupedLanguageOptions } from './language-picker-options';
12
13
  const pickerOptionStyles = null;
13
14
  const styles = {
14
15
  divider: "_h7alglyw _179rglyw _mqm2ia51 _1bsb1osq",
@@ -47,29 +48,60 @@ const menuPopperProps = {
47
48
  enabled: false
48
49
  }]
49
50
  };
51
+ const getRecentlyUsedLanguages = (recentLanguageValues, optionsByValue) => {
52
+ const recentlyUsedLanguages = [];
53
+ for (const recentLanguageValue of recentLanguageValues) {
54
+ const option = optionsByValue.get(recentLanguageValue);
55
+ if (option) {
56
+ recentlyUsedLanguages.push(option);
57
+ }
58
+ }
59
+ return recentlyUsedLanguages;
60
+ };
50
61
  export const LanguagePicker = ({
51
62
  api,
52
63
  defaultValue,
53
64
  editorView,
54
65
  filterOption,
55
66
  formatMessage,
56
- options
67
+ languagePickerOptions,
68
+ recentLanguageValues = [],
69
+ onLanguageSelect,
70
+ onMenuOpen
57
71
  }) => {
58
72
  var _api$analytics, _defaultValue$label;
59
73
  const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
60
74
  const label = (_defaultValue$label = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.label) !== null && _defaultValue$label !== void 0 ? _defaultValue$label : formatMessage(codeBlockButtonMessages.selectLanguage);
61
75
  const selectLanguageLabel = formatMessage(codeBlockButtonMessages.selectLanguage);
62
- const [inputValue, setInputValue] = useState('');
76
+ const [hasSearchQuery, setHasSearchQuery] = useState(false);
77
+ const inputValueRef = useRef('');
78
+ const optionsByValue = useMemo(() => new Map(languagePickerOptions.map(option => [option.value, option])), [languagePickerOptions]);
79
+ const recentlyUsedLanguages = useMemo(() => getRecentlyUsedLanguages(recentLanguageValues, optionsByValue), [recentLanguageValues, optionsByValue]);
80
+ const options = useMemo(() => createGroupedLanguageOptions({
81
+ formatMessage,
82
+ languages: languagePickerOptions,
83
+ recentlyUsedLanguages
84
+ }), [formatMessage, languagePickerOptions, recentlyUsedLanguages]);
63
85
  const searchOptions = useMemo(() => options.flatMap(group => group.options), [options]);
64
- const hasSearchQuery = inputValue.trim().length > 0;
65
86
  const handleChange = useCallback(option => {
87
+ var _option$selectionSour;
66
88
  if (!option) {
67
89
  return;
68
90
  }
69
- changeLanguage(editorAnalyticsAPI)(option.value)(editorView.state, editorView.dispatch);
70
- }, [editorAnalyticsAPI, editorView]);
71
- const handleInputChange = useCallback(newInputValue => {
72
- setInputValue(newInputValue);
91
+ const isSearchSelection = inputValueRef.current.trim().length > 0;
92
+ const selectionSource = isSearchSelection ? 'search' : (_option$selectionSour = option.selectionSource) !== null && _option$selectionSour !== void 0 ? _option$selectionSour : 'all';
93
+ const commandSucceeded = changeLanguage(editorAnalyticsAPI)(option.value, selectionSource)(editorView.state, editorView.dispatch);
94
+ if (commandSucceeded) {
95
+ onLanguageSelect === null || onLanguageSelect === void 0 ? void 0 : onLanguageSelect(option.value);
96
+ }
97
+ }, [editorAnalyticsAPI, editorView, onLanguageSelect]);
98
+ const handleInputChange = useCallback((newInputValue, actionMeta) => {
99
+ // React-select clears the input as part of selecting a value before onChange fires.
100
+ // Keep the last user-typed query so handleChange can report search selections correctly.
101
+ if (!actionMeta || actionMeta.action === 'input-change') {
102
+ inputValueRef.current = newInputValue;
103
+ }
104
+ setHasSearchQuery(newInputValue.trim().length > 0);
73
105
  return newInputValue;
74
106
  }, []);
75
107
  const renderTarget = useCallback(({
@@ -93,13 +125,13 @@ export const LanguagePicker = ({
93
125
  return /*#__PURE__*/React.createElement(PopupSelect, {
94
126
  components: popupSelectComponents,
95
127
  filterOption: filterOption,
96
- inputValue: inputValue,
97
128
  label: selectLanguageLabel,
98
129
  maxMenuHeight: 300,
99
130
  minMenuWidth: 200,
100
131
  menuPlacement: "auto",
101
132
  onChange: handleChange,
102
133
  onInputChange: handleInputChange,
134
+ onMenuOpen: onMenuOpen,
103
135
  options: hasSearchQuery ? searchOptions : options,
104
136
  popperProps: menuPopperProps,
105
137
  searchThreshold: -1,
@@ -5,6 +5,7 @@ export const PLAIN_TEXT_LANGUAGE_VALUE = 'text';
5
5
  export const getDetectLanguageOption = formatMessage => ({
6
6
  alias: [DETECT_LANGUAGE_VALUE],
7
7
  label: formatMessage(codeBlockButtonMessages.detectLanguage),
8
+ selectionSource: 'pinned',
8
9
  value: DETECT_LANGUAGE_VALUE
9
10
  });
10
11
  export const createGroupedLanguageOptions = ({
@@ -12,20 +13,30 @@ export const createGroupedLanguageOptions = ({
12
13
  languages,
13
14
  recentlyUsedLanguages = []
14
15
  }) => {
15
- const allLanguages = languages.filter(language => language.value !== NONE_LANGUAGE_VALUE && language.value !== PLAIN_TEXT_LANGUAGE_VALUE);
16
+ const recentlyUsedLanguageValues = new Set(recentlyUsedLanguages.map(language => language.value));
17
+ const allLanguages = languages.filter(language => language.value !== NONE_LANGUAGE_VALUE && language.value !== PLAIN_TEXT_LANGUAGE_VALUE && !recentlyUsedLanguageValues.has(language.value));
16
18
  const plainTextOption = languages.find(language => language.value === PLAIN_TEXT_LANGUAGE_VALUE);
17
19
  const pinnedOptions = [getDetectLanguageOption(formatMessage)];
18
20
  if (plainTextOption) {
19
- pinnedOptions.push(plainTextOption);
21
+ pinnedOptions.push({
22
+ ...plainTextOption,
23
+ selectionSource: 'pinned'
24
+ });
20
25
  }
21
26
  return [{
22
27
  label: '',
23
28
  options: pinnedOptions
24
29
  }, ...(recentlyUsedLanguages.length > 0 ? [{
25
30
  label: formatMessage(codeBlockButtonMessages.recentlyUsed),
26
- options: recentlyUsedLanguages
31
+ options: recentlyUsedLanguages.map(language => ({
32
+ ...language,
33
+ selectionSource: 'recentlyUsed'
34
+ }))
27
35
  }] : []), {
28
36
  label: formatMessage(codeBlockButtonMessages.all),
29
- options: allLanguages
37
+ options: allLanguages.map(language => ({
38
+ ...language,
39
+ selectionSource: 'all'
40
+ }))
30
41
  }];
31
42
  };
@@ -0,0 +1,38 @@
1
+ import { StorageClient } from '@atlaskit/frontend-utilities/storage-client';
2
+ import { DEFAULT_LANGUAGES, getLanguageIdentifier } from '../pm-plugins/language-list';
3
+ import { DETECT_LANGUAGE_VALUE, NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE } from './language-picker-options';
4
+ export const RECENT_LANGUAGES_STORAGE_KEY = 'recently-used-languages';
5
+ const RECENT_LANGUAGES_STORAGE_CLIENT_KEY = '@atlaskit/editor-plugin-code-block';
6
+ const MAX_RECENT_LANGUAGES = 3;
7
+ const RECENT_LANGUAGE_BLOCKLIST = new Set([DETECT_LANGUAGE_VALUE, NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE]);
8
+ const KNOWN_LANGUAGE_VALUES = new Set(DEFAULT_LANGUAGES.map(language => getLanguageIdentifier(language)));
9
+ const recentLanguagesStorageClient = new StorageClient(RECENT_LANGUAGES_STORAGE_CLIENT_KEY);
10
+ const isValidRecentLanguage = language => KNOWN_LANGUAGE_VALUES.has(language) && !RECENT_LANGUAGE_BLOCKLIST.has(language);
11
+ const readRecentLanguages = () => {
12
+ const storedValue = recentLanguagesStorageClient.getItem(RECENT_LANGUAGES_STORAGE_KEY);
13
+ return Array.isArray(storedValue) ? storedValue : [];
14
+ };
15
+ export const getRecentLanguages = () => {
16
+ try {
17
+ return readRecentLanguages();
18
+ } catch {
19
+ return [];
20
+ }
21
+ };
22
+ export const saveRecentLanguage = language => {
23
+ if (!isValidRecentLanguage(language)) {
24
+ return;
25
+ }
26
+ try {
27
+ const recentLanguages = readRecentLanguages();
28
+ const nextRecentLanguages = Array.from(new Set([language, ...recentLanguages])).slice(0, MAX_RECENT_LANGUAGES);
29
+
30
+ // StorageClient only exposes setItemWithExpiry; omitting the duration stores without expiry.
31
+ recentLanguagesStorageClient.setItemWithExpiry(RECENT_LANGUAGES_STORAGE_KEY, nextRecentLanguages);
32
+ } catch {
33
+ return;
34
+ }
35
+ };
36
+ export const clearRecentLanguages = () => {
37
+ recentLanguagesStorageClient.removeItem(RECENT_LANGUAGES_STORAGE_KEY);
38
+ };
@@ -40,7 +40,7 @@ export var removeCodeBlock = function removeCodeBlock(state, dispatch) {
40
40
  return true;
41
41
  };
42
42
  export var changeLanguage = function changeLanguage(editorAnalyticsAPI) {
43
- return function (language) {
43
+ return function (language, selectionSource) {
44
44
  return function (state, dispatch) {
45
45
  var _pluginKey$getState;
46
46
  var codeBlock = state.schema.nodes.codeBlock;
@@ -58,9 +58,11 @@ export var changeLanguage = function changeLanguage(editorAnalyticsAPI) {
58
58
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
59
59
  action: ACTION.LANGUAGE_SELECTED,
60
60
  actionSubject: ACTION_SUBJECT.CODE_BLOCK,
61
- attributes: {
61
+ attributes: _objectSpread({
62
62
  language: language !== null && language !== void 0 ? language : 'none'
63
- },
63
+ }, selectionSource ? {
64
+ selectionSource: selectionSource
65
+ } : {}),
64
66
  eventType: EVENT_TYPE.TRACK
65
67
  })(result);
66
68
  dispatch(result);