overtype 2.1.0 → 2.2.0

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.
package/src/shortcuts.js CHANGED
@@ -1,18 +1,14 @@
1
1
  /**
2
2
  * Keyboard shortcuts handler for OverType editor
3
- * Uses the same handleAction method as toolbar for consistency
3
+ * Delegates to editor.performAction for consistent behavior
4
4
  */
5
5
 
6
- import * as markdownActions from 'markdown-actions';
7
-
8
6
  /**
9
7
  * ShortcutsManager - Handles keyboard shortcuts for the editor
10
8
  */
11
9
  export class ShortcutsManager {
12
10
  constructor(editor) {
13
11
  this.editor = editor;
14
- this.textarea = editor.textarea;
15
- // No need to add our own listener - OverType will call handleKeydown
16
12
  }
17
13
 
18
14
  /**
@@ -26,100 +22,39 @@ export class ShortcutsManager {
26
22
 
27
23
  if (!modKey) return false;
28
24
 
29
- let action = null;
25
+ let actionId = null;
30
26
 
31
- // Map keyboard shortcuts to toolbar actions
32
- switch(event.key.toLowerCase()) {
27
+ switch (event.key.toLowerCase()) {
33
28
  case 'b':
34
- if (!event.shiftKey) {
35
- action = 'toggleBold';
36
- }
29
+ if (!event.shiftKey) actionId = 'toggleBold';
37
30
  break;
38
-
39
31
  case 'i':
40
- if (!event.shiftKey) {
41
- action = 'toggleItalic';
42
- }
32
+ if (!event.shiftKey) actionId = 'toggleItalic';
43
33
  break;
44
-
45
34
  case 'k':
46
- if (!event.shiftKey) {
47
- action = 'insertLink';
48
- }
35
+ if (!event.shiftKey) actionId = 'insertLink';
49
36
  break;
50
-
51
37
  case '7':
52
- if (event.shiftKey) {
53
- action = 'toggleNumberedList';
54
- }
38
+ if (event.shiftKey) actionId = 'toggleNumberedList';
55
39
  break;
56
-
57
40
  case '8':
58
- if (event.shiftKey) {
59
- action = 'toggleBulletList';
60
- }
41
+ if (event.shiftKey) actionId = 'toggleBulletList';
61
42
  break;
62
43
  }
63
44
 
64
- // If we have an action, handle it exactly like the toolbar does
65
- if (action) {
45
+ if (actionId) {
66
46
  event.preventDefault();
67
-
68
- // If toolbar exists, use its handleAction method (exact same code path)
69
- if (this.editor.toolbar) {
70
- this.editor.toolbar.handleAction(action);
71
- } else {
72
- // Fallback: duplicate the toolbar's handleAction logic
73
- this.handleAction(action);
74
- }
75
-
47
+ this.editor.performAction(actionId, event);
76
48
  return true;
77
49
  }
78
50
 
79
51
  return false;
80
52
  }
81
53
 
82
- /**
83
- * Handle action - fallback when no toolbar exists
84
- * This duplicates toolbar.handleAction for consistency
85
- */
86
- async handleAction(action) {
87
- const textarea = this.textarea;
88
- if (!textarea) return;
89
-
90
- // Focus textarea
91
- textarea.focus();
92
-
93
- try {
94
- switch (action) {
95
- case 'toggleBold':
96
- markdownActions.toggleBold(textarea);
97
- break;
98
- case 'toggleItalic':
99
- markdownActions.toggleItalic(textarea);
100
- break;
101
- case 'insertLink':
102
- markdownActions.insertLink(textarea);
103
- break;
104
- case 'toggleBulletList':
105
- markdownActions.toggleBulletList(textarea);
106
- break;
107
- case 'toggleNumberedList':
108
- markdownActions.toggleNumberedList(textarea);
109
- break;
110
- }
111
-
112
- // Trigger input event to update preview
113
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
114
- } catch (error) {
115
- console.error('Error in markdown action:', error);
116
- }
117
- }
118
-
119
54
  /**
120
55
  * Cleanup
121
56
  */
122
57
  destroy() {
123
58
  // Nothing to clean up since we don't add our own listener
124
59
  }
125
- }
60
+ }
package/src/styles.js CHANGED
@@ -211,17 +211,36 @@ export function generateStyles(options = {}) {
211
211
  /* Prevent mobile zoom on focus */
212
212
  touch-action: manipulation !important;
213
213
 
214
- /* Disable autofill and spellcheck */
214
+ /* Disable autofill */
215
215
  autocomplete: off !important;
216
216
  autocorrect: off !important;
217
217
  autocapitalize: off !important;
218
- spellcheck: false !important;
219
218
  }
220
219
 
221
220
  .overtype-wrapper .overtype-input::selection {
222
221
  background-color: var(--selection, rgba(244, 211, 94, 0.4));
223
222
  }
224
223
 
224
+ /* Placeholder shim - visible when textarea is empty */
225
+ .overtype-wrapper .overtype-placeholder {
226
+ position: absolute !important;
227
+ top: 0 !important;
228
+ left: 0 !important;
229
+ width: 100% !important;
230
+ z-index: 0 !important;
231
+ pointer-events: none !important;
232
+ user-select: none !important;
233
+ font-family: ${fontFamily} !important;
234
+ font-size: var(--instance-font-size, ${fontSize}) !important;
235
+ line-height: var(--instance-line-height, ${lineHeight}) !important;
236
+ padding: var(--instance-padding, ${padding}) !important;
237
+ box-sizing: border-box !important;
238
+ color: var(--placeholder, #999) !important;
239
+ overflow: hidden !important;
240
+ white-space: nowrap !important;
241
+ text-overflow: ellipsis !important;
242
+ }
243
+
225
244
  /* Preview layer styles */
226
245
  .overtype-wrapper .overtype-preview {
227
246
  /* Layer positioning */
@@ -489,6 +508,10 @@ export function generateStyles(options = {}) {
489
508
 
490
509
 
491
510
  /* Toolbar Styles */
511
+ .overtype-toolbar.overtype-toolbar-hidden {
512
+ display: none !important;
513
+ }
514
+
492
515
  .overtype-toolbar {
493
516
  display: flex !important;
494
517
  align-items: center !important;
@@ -763,19 +786,14 @@ export function generateStyles(options = {}) {
763
786
 
764
787
  /* Code blocks - proper pre/code styling in preview mode */
765
788
  .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block {
766
- background: #2d2d2d !important;
767
- color: #f8f8f2 !important;
789
+ background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;
790
+ color: var(--code, #0d3b66) !important;
768
791
  padding: 1.2em !important;
769
792
  border-radius: 3px !important;
770
793
  overflow-x: auto !important;
771
794
  margin: 0 !important;
772
795
  display: block !important;
773
796
  }
774
-
775
- /* Cave theme code block background in preview mode */
776
- .overtype-container[data-theme="cave"][data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block {
777
- background: #11171F !important;
778
- }
779
797
 
780
798
  .overtype-container[data-mode="preview"] .overtype-wrapper .overtype-preview pre.code-block code {
781
799
  background: transparent !important;
@@ -840,16 +858,17 @@ export function generateStyles(options = {}) {
840
858
  height: 2px !important;
841
859
  }
842
860
 
843
- /* Link Tooltip - Base styles (all browsers) */
861
+ /* Link Tooltip */
844
862
  .overtype-link-tooltip {
845
- /* Visual styles that work for both positioning methods */
846
863
  background: #333 !important;
847
864
  color: white !important;
848
865
  padding: 6px 10px !important;
849
866
  border-radius: 16px !important;
850
867
  font-size: 12px !important;
851
868
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
852
- display: none !important;
869
+ display: flex !important;
870
+ visibility: hidden !important;
871
+ pointer-events: none !important;
853
872
  z-index: 10000 !important;
854
873
  cursor: pointer !important;
855
874
  box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
@@ -857,25 +876,14 @@ export function generateStyles(options = {}) {
857
876
  white-space: nowrap !important;
858
877
  overflow: hidden !important;
859
878
  text-overflow: ellipsis !important;
860
-
861
- /* Base positioning for Floating UI fallback */
862
- position: absolute;
879
+ position: fixed;
880
+ top: 0;
881
+ left: 0;
863
882
  }
864
883
 
865
884
  .overtype-link-tooltip.visible {
866
- display: flex !important;
867
- }
868
-
869
- /* CSS Anchor Positioning (modern browsers only) */
870
- @supports (position-anchor: --x) and (position-area: center) {
871
- .overtype-link-tooltip {
872
- /* Only anchor positioning specific properties */
873
- position-anchor: var(--target-anchor, --link-0);
874
- position-area: block-end center;
875
- margin-top: 8px !important;
876
- position-try: most-width block-end inline-end, flip-inline, block-start center;
877
- position-visibility: anchors-visible;
878
- }
885
+ visibility: visible !important;
886
+ pointer-events: auto !important;
879
887
  }
880
888
 
881
889
  ${mobileStyles}
package/src/themes.js CHANGED
@@ -39,6 +39,7 @@ export const solar = {
39
39
  toolbarIcon: '#0d3b66', // Yale Blue - icon color
40
40
  toolbarHover: '#f5f5f5', // Light gray - hover background
41
41
  toolbarActive: '#faf0ca', // Lemon Chiffon - active button background
42
+ placeholder: '#999999', // Gray - placeholder text
42
43
  }
43
44
  };
44
45
 
@@ -78,6 +79,7 @@ export const cave = {
78
79
  toolbarIcon: '#c5dde8', // Light blue-gray - icon color
79
80
  toolbarHover: '#243546', // Slightly lighter charcoal - hover background
80
81
  toolbarActive: '#2a3f52', // Even lighter - active button background
82
+ placeholder: '#6a7a88', // Muted blue-gray - placeholder text
81
83
  }
82
84
  };
83
85
 
@@ -87,6 +89,7 @@ export const cave = {
87
89
  export const themes = {
88
90
  solar,
89
91
  cave,
92
+ auto: solar,
90
93
  // Aliases for backward compatibility
91
94
  light: solar,
92
95
  dark: cave
@@ -106,6 +109,17 @@ export function getTheme(theme) {
106
109
  return theme;
107
110
  }
108
111
 
112
+ /**
113
+ * Resolve auto theme to actual theme based on system color scheme
114
+ * @param {string} themeName - Theme name to resolve
115
+ * @returns {string} Resolved theme name ('solar' or 'cave' if auto, otherwise unchanged)
116
+ */
117
+ export function resolveAutoTheme(themeName) {
118
+ if (themeName !== 'auto') return themeName;
119
+ const mq = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
120
+ return mq?.matches ? 'cave' : 'solar';
121
+ }
122
+
109
123
  /**
110
124
  * Apply theme colors to CSS variables
111
125
  * @param {Object} colors - Theme colors object
@@ -8,15 +8,18 @@ import * as markdownActions from 'markdown-actions';
8
8
 
9
9
  /**
10
10
  * Built-in toolbar button definitions
11
- * Each button has: name, icon, title, action
11
+ * Each button has: name, actionId, icon, title, action
12
+ * - name: DOM identifier for the button element
13
+ * - actionId: Canonical action identifier used by performAction
12
14
  * Action signature: ({ editor, getValue, setValue, event }) => void
13
15
  */
14
16
  export const toolbarButtons = {
15
17
  bold: {
16
18
  name: 'bold',
19
+ actionId: 'toggleBold',
17
20
  icon: icons.boldIcon,
18
21
  title: 'Bold (Ctrl+B)',
19
- action: ({ editor, event }) => {
22
+ action: ({ editor }) => {
20
23
  markdownActions.toggleBold(editor.textarea);
21
24
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
22
25
  }
@@ -24,9 +27,10 @@ export const toolbarButtons = {
24
27
 
25
28
  italic: {
26
29
  name: 'italic',
30
+ actionId: 'toggleItalic',
27
31
  icon: icons.italicIcon,
28
32
  title: 'Italic (Ctrl+I)',
29
- action: ({ editor, event }) => {
33
+ action: ({ editor }) => {
30
34
  markdownActions.toggleItalic(editor.textarea);
31
35
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
32
36
  }
@@ -34,9 +38,10 @@ export const toolbarButtons = {
34
38
 
35
39
  code: {
36
40
  name: 'code',
41
+ actionId: 'toggleCode',
37
42
  icon: icons.codeIcon,
38
43
  title: 'Inline Code',
39
- action: ({ editor, event }) => {
44
+ action: ({ editor }) => {
40
45
  markdownActions.toggleCode(editor.textarea);
41
46
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
42
47
  }
@@ -49,9 +54,10 @@ export const toolbarButtons = {
49
54
 
50
55
  link: {
51
56
  name: 'link',
57
+ actionId: 'insertLink',
52
58
  icon: icons.linkIcon,
53
59
  title: 'Insert Link',
54
- action: ({ editor, event }) => {
60
+ action: ({ editor }) => {
55
61
  markdownActions.insertLink(editor.textarea);
56
62
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
57
63
  }
@@ -59,9 +65,10 @@ export const toolbarButtons = {
59
65
 
60
66
  h1: {
61
67
  name: 'h1',
68
+ actionId: 'toggleH1',
62
69
  icon: icons.h1Icon,
63
70
  title: 'Heading 1',
64
- action: ({ editor, event }) => {
71
+ action: ({ editor }) => {
65
72
  markdownActions.toggleH1(editor.textarea);
66
73
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
67
74
  }
@@ -69,9 +76,10 @@ export const toolbarButtons = {
69
76
 
70
77
  h2: {
71
78
  name: 'h2',
79
+ actionId: 'toggleH2',
72
80
  icon: icons.h2Icon,
73
81
  title: 'Heading 2',
74
- action: ({ editor, event }) => {
82
+ action: ({ editor }) => {
75
83
  markdownActions.toggleH2(editor.textarea);
76
84
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
77
85
  }
@@ -79,9 +87,10 @@ export const toolbarButtons = {
79
87
 
80
88
  h3: {
81
89
  name: 'h3',
90
+ actionId: 'toggleH3',
82
91
  icon: icons.h3Icon,
83
92
  title: 'Heading 3',
84
- action: ({ editor, event }) => {
93
+ action: ({ editor }) => {
85
94
  markdownActions.toggleH3(editor.textarea);
86
95
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
87
96
  }
@@ -89,9 +98,10 @@ export const toolbarButtons = {
89
98
 
90
99
  bulletList: {
91
100
  name: 'bulletList',
101
+ actionId: 'toggleBulletList',
92
102
  icon: icons.bulletListIcon,
93
103
  title: 'Bullet List',
94
- action: ({ editor, event }) => {
104
+ action: ({ editor }) => {
95
105
  markdownActions.toggleBulletList(editor.textarea);
96
106
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
97
107
  }
@@ -99,9 +109,10 @@ export const toolbarButtons = {
99
109
 
100
110
  orderedList: {
101
111
  name: 'orderedList',
112
+ actionId: 'toggleNumberedList',
102
113
  icon: icons.orderedListIcon,
103
114
  title: 'Numbered List',
104
- action: ({ editor, event }) => {
115
+ action: ({ editor }) => {
105
116
  markdownActions.toggleNumberedList(editor.textarea);
106
117
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
107
118
  }
@@ -109,9 +120,10 @@ export const toolbarButtons = {
109
120
 
110
121
  taskList: {
111
122
  name: 'taskList',
123
+ actionId: 'toggleTaskList',
112
124
  icon: icons.taskListIcon,
113
125
  title: 'Task List',
114
- action: ({ editor, event }) => {
126
+ action: ({ editor }) => {
115
127
  if (markdownActions.toggleTaskList) {
116
128
  markdownActions.toggleTaskList(editor.textarea);
117
129
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -121,14 +133,38 @@ export const toolbarButtons = {
121
133
 
122
134
  quote: {
123
135
  name: 'quote',
136
+ actionId: 'toggleQuote',
124
137
  icon: icons.quoteIcon,
125
138
  title: 'Quote',
126
- action: ({ editor, event }) => {
139
+ action: ({ editor }) => {
127
140
  markdownActions.toggleQuote(editor.textarea);
128
141
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
129
142
  }
130
143
  },
131
144
 
145
+ upload: {
146
+ name: 'upload',
147
+ actionId: 'uploadFile',
148
+ icon: icons.uploadIcon,
149
+ title: 'Upload File',
150
+ action: ({ editor }) => {
151
+ if (!editor.options.fileUpload?.enabled) return;
152
+ const input = document.createElement('input');
153
+ input.type = 'file';
154
+ input.multiple = true;
155
+ if (editor.options.fileUpload.mimeTypes?.length > 0) {
156
+ input.accept = editor.options.fileUpload.mimeTypes.join(',');
157
+ }
158
+ input.onchange = () => {
159
+ if (!input.files?.length) return;
160
+ const dt = new DataTransfer();
161
+ for (const f of input.files) dt.items.add(f);
162
+ editor._handleDataTransfer(dt);
163
+ };
164
+ input.click();
165
+ }
166
+ },
167
+
132
168
  viewMode: {
133
169
  name: 'viewMode',
134
170
  icon: icons.eyeIcon,
package/src/toolbar.js CHANGED
@@ -73,39 +73,11 @@ export class Toolbar {
73
73
  return button;
74
74
  }
75
75
 
76
- // Standard button click handler
77
- button._clickHandler = async (e) => {
76
+ // Standard button click handler - delegate to performAction
77
+ button._clickHandler = (e) => {
78
78
  e.preventDefault();
79
-
80
- // Focus textarea before action
81
- this.editor.textarea.focus();
82
-
83
- try {
84
- if (buttonConfig.action) {
85
- // Call action with consistent context object
86
- await buttonConfig.action({
87
- editor: this.editor,
88
- getValue: () => this.editor.getValue(),
89
- setValue: (value) => this.editor.setValue(value),
90
- event: e
91
- });
92
- }
93
- } catch (error) {
94
- console.error(`Button "${buttonConfig.name}" error:`, error);
95
-
96
- // Dispatch error event
97
- this.editor.wrapper.dispatchEvent(new CustomEvent('button-error', {
98
- detail: { buttonName: buttonConfig.name, error }
99
- }));
100
-
101
- // Visual feedback
102
- button.classList.add('button-error');
103
- button.style.animation = 'buttonError 0.3s';
104
- setTimeout(() => {
105
- button.classList.remove('button-error');
106
- button.style.animation = '';
107
- }, 300);
108
- }
79
+ const actionId = buttonConfig.actionId || buttonConfig.name;
80
+ this.editor.performAction(actionId, e);
109
81
  };
110
82
 
111
83
  button.addEventListener('click', button._clickHandler);
@@ -113,31 +85,38 @@ export class Toolbar {
113
85
  }
114
86
 
115
87
  /**
116
- * Handle button action programmatically (used by keyboard shortcuts)
117
- * @param {Object} buttonConfig - Button configuration object with action function
88
+ * Handle button action programmatically
89
+ * Accepts either an actionId string or a buttonConfig object (backwards compatible)
90
+ * @param {string|Object} actionIdOrConfig - Action identifier string or button config object
91
+ * @returns {Promise<boolean>} Whether the action was executed
118
92
  */
119
- async handleAction(buttonConfig) {
120
- // Focus textarea before action
121
- this.editor.textarea.focus();
122
-
123
- try {
124
- if (buttonConfig.action) {
125
- // Call action with consistent context object
126
- await buttonConfig.action({
93
+ async handleAction(actionIdOrConfig) {
94
+ // Old style: buttonConfig object with .action function - execute directly
95
+ if (actionIdOrConfig && typeof actionIdOrConfig === 'object' && typeof actionIdOrConfig.action === 'function') {
96
+ this.editor.textarea.focus();
97
+ try {
98
+ await actionIdOrConfig.action({
127
99
  editor: this.editor,
128
100
  getValue: () => this.editor.getValue(),
129
101
  setValue: (value) => this.editor.setValue(value),
130
102
  event: null
131
103
  });
104
+ return true;
105
+ } catch (error) {
106
+ console.error(`Action "${actionIdOrConfig.name}" error:`, error);
107
+ this.editor.wrapper.dispatchEvent(new CustomEvent('button-error', {
108
+ detail: { buttonName: actionIdOrConfig.name, error }
109
+ }));
110
+ return false;
132
111
  }
133
- } catch (error) {
134
- console.error(`Action "${buttonConfig.name}" error:`, error);
112
+ }
135
113
 
136
- // Dispatch error event
137
- this.editor.wrapper.dispatchEvent(new CustomEvent('button-error', {
138
- detail: { buttonName: buttonConfig.name, error }
139
- }));
114
+ // New style: string actionId - delegate to performAction
115
+ if (typeof actionIdOrConfig === 'string') {
116
+ return this.editor.performAction(actionIdOrConfig, null);
140
117
  }
118
+
119
+ return false;
141
120
  }
142
121
 
143
122
  /**
@@ -308,6 +287,18 @@ export class Toolbar {
308
287
  }
309
288
  }
310
289
 
290
+ show() {
291
+ if (this.container) {
292
+ this.container.classList.remove('overtype-toolbar-hidden');
293
+ }
294
+ }
295
+
296
+ hide() {
297
+ if (this.container) {
298
+ this.container.classList.add('overtype-toolbar-hidden');
299
+ }
300
+ }
301
+
311
302
  /**
312
303
  * Destroy toolbar and cleanup
313
304
  */