draftail-text-utils 3.0.2__tar.gz → 3.0.4__tar.gz

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 (50) hide show
  1. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/PKG-INFO +1 -1
  2. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/pyproject.toml +2 -2
  3. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/pyproject.toml.orig +2 -2
  4. draftail_text_utils-3.0.4/src/draftail_text_utils/__init__.py +1 -0
  5. draftail_text_utils-3.0.4/src/draftail_text_utils/rich_text/text_alignment.py +197 -0
  6. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/common.css +1 -0
  7. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/font_size.js +122 -65
  8. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/text_alignment.js +96 -5
  9. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/wagtail_hooks.py +3 -1
  10. draftail_text_utils-3.0.2/src/draftail_text_utils/__init__.py +0 -1
  11. draftail_text_utils-3.0.2/src/draftail_text_utils/rich_text/text_alignment.py +0 -70
  12. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/LICENSE +0 -0
  13. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/README.md +0 -0
  14. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/apps.py +0 -0
  15. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/conf.py +0 -0
  16. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/migrations/__init__.py +0 -0
  17. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/__init__.py +0 -0
  18. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/base.py +0 -0
  19. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/dynamic_link.py +0 -0
  20. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/font_family.py +0 -0
  21. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/font_size.py +0 -0
  22. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/highlight_color.py +0 -0
  23. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/text_color.py +0 -0
  24. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/rich_text/text_style.py +0 -0
  25. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/color.css +0 -0
  26. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/dynamic_link.css +0 -0
  27. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/font_family.css +0 -0
  28. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/font_size.css +0 -0
  29. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/highlight_color.css +0 -0
  30. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/page.css +0 -0
  31. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/text_alignment.css +0 -0
  32. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/css/text_color.css +0 -0
  33. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/common.js +0 -0
  34. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/dynamic_link.js +0 -0
  35. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/font_family.js +0 -0
  36. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/highlight_color.js +0 -0
  37. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/styled_link_source.js +0 -0
  38. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/text_color.js +0 -0
  39. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/static/draftail_text_utils/js/text_style_entity.js +0 -0
  40. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/align-center.svg +0 -0
  41. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/align-justify.svg +0 -0
  42. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/align-left.svg +0 -0
  43. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/align-right.svg +0 -0
  44. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/anchor.svg +0 -0
  45. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/font.svg +0 -0
  46. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/highlighter.svg +0 -0
  47. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/palette.svg +0 -0
  48. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templates/draftail_text_utils/icons/text-height.svg +0 -0
  49. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templatetags/__init__.py +0 -0
  50. {draftail_text_utils-3.0.2 → draftail_text_utils-3.0.4}/src/draftail_text_utils/templatetags/draftail_text_utils_tags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftail-text-utils
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Extends Draftail's text customization
5
5
  Keywords: draftail,font family,font size,highlight,rich text,text alignment,text color,wagtail
6
6
  Author: baldwinboy
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "draftail-text-utils"
3
- version = "3.0.2"
3
+ version = "3.0.4"
4
4
  description = "Extends Draftail's text customization"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -50,7 +50,7 @@ Repository = "https://github.com/baldwinboy/draftail_text_utils"
50
50
  dev = [
51
51
  "dj-database-url==3.1.2",
52
52
  "pre-commit==4.7.0",
53
- "prek==0.5.5",
53
+ "prek==0.5.6",
54
54
  "pytest==9.1.1",
55
55
  "pytest-cov==7.1.0",
56
56
  "pytest-django==4.14.0",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "draftail-text-utils"
3
- version = "3.0.2"
3
+ version = "3.0.4"
4
4
  description = "Extends Draftail's text customization"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -41,7 +41,7 @@ Repository = "https://github.com/baldwinboy/draftail_text_utils"
41
41
  dev = [
42
42
  "dj-database-url==3.1.2",
43
43
  "pre-commit==4.7.0",
44
- "prek==0.5.5",
44
+ "prek==0.5.6",
45
45
  "pytest==9.1.1",
46
46
  "pytest-cov==7.1.0",
47
47
  "pytest-django==4.14.0",
@@ -0,0 +1 @@
1
+ __version__ = "3.0.4"
@@ -0,0 +1,197 @@
1
+ """
2
+ Draftail rich-text feature: Text alignment.
3
+
4
+ Registers:
5
+ - Legacy ``BlockFeature``s for old, block-type-based alignment content.
6
+ - A ``"text-alignment"`` ControlFeature for the toolbar button group.
7
+ - Converter rules that store alignment in block data, so alignment can
8
+ coexist with heading, list, and other default block types.
9
+ """
10
+
11
+ from django.utils.translation import gettext_lazy as _
12
+ from draftjs_exporter.defaults import render_children
13
+ from draftjs_exporter.dom import DOM
14
+ from wagtail.admin.rich_text.converters.html_to_contentstate import (
15
+ BLOCK_KEY_NAME,
16
+ Block,
17
+ BlockElementHandler,
18
+ )
19
+
20
+ from draftail_text_utils.conf import feature_enabled
21
+
22
+ from .base import (
23
+ control_json_script,
24
+ register_block_feature,
25
+ register_control_feature,
26
+ )
27
+
28
+
29
+ ALIGNMENTS = [
30
+ ("ALIGNMENT_LEFT", "left", "start", _("Left")),
31
+ ("ALIGNMENT_CENTER", "center", "center", _("Center")),
32
+ ("ALIGNMENT_RIGHT", "right", "end", _("Right")),
33
+ ("ALIGNMENT_JUSTIFY", "justify", "justify", _("Justify")),
34
+ ]
35
+
36
+ ALIGNMENT_STYLES = {
37
+ value: style_value for _type, value, style_value, _label in ALIGNMENTS
38
+ }
39
+ STYLE_ALIGNMENTS = {
40
+ style_value: value for _type, value, style_value, _label in ALIGNMENTS
41
+ }
42
+
43
+ BLOCK_TAGS = [
44
+ ("unstyled", "p"),
45
+ ("header-one", "h1"),
46
+ ("header-two", "h2"),
47
+ ("header-three", "h3"),
48
+ ("header-four", "h4"),
49
+ ("header-five", "h5"),
50
+ ("header-six", "h6"),
51
+ ("blockquote", "blockquote"),
52
+ ("code-block", "pre"),
53
+ ]
54
+
55
+ LIST_TAGS = [
56
+ ("ordered-list-item", "ol"),
57
+ ("unordered-list-item", "ul"),
58
+ ]
59
+
60
+
61
+ class AlignmentBlock(Block):
62
+ """A block that persists alignment in its Draft.js ``data`` field."""
63
+
64
+ def __init__(self, typ, depth=0, key=None, alignment=None):
65
+ super().__init__(typ, depth, key)
66
+ self.data = {"alignment": alignment or "left"}
67
+
68
+ def as_dict(self):
69
+ return super().as_dict() | {"data": self.data}
70
+
71
+
72
+ class AlignmentHandler(BlockElementHandler):
73
+ """Database HTML to ContentState, preserving the original block type."""
74
+
75
+ def __init__(self, block_type=None):
76
+ self.block_type = block_type
77
+
78
+ def create_block(self, name, attrs, state, contentstate):
79
+ block_type = self.block_type or state.list_item_type or "unstyled"
80
+ style_value = attrs.get("data-dtu-text-align", "start")
81
+ alignment = STYLE_ALIGNMENTS.get(style_value, "left")
82
+
83
+ return AlignmentBlock(
84
+ block_type,
85
+ depth=state.list_depth,
86
+ key=attrs.get(BLOCK_KEY_NAME),
87
+ alignment=alignment,
88
+ )
89
+
90
+
91
+ def _alignment_element(tag_name):
92
+ """
93
+ Build a Draft.js exporter element function for ``tag_name``.
94
+
95
+ Alignment is read from block data and only added to the output when
96
+ present, so unaligned blocks keep their normal database representation.
97
+ """
98
+
99
+ def element(props):
100
+ block = props.get("block") or {}
101
+ data = block.get("data") or {}
102
+ alignment = data.get("alignment")
103
+
104
+ attrs = {}
105
+ if alignment:
106
+ style_value = ALIGNMENT_STYLES.get(alignment)
107
+ if style_value:
108
+ attrs["class"] = "dtu-block_text-align"
109
+ attrs["data-dtu-text-align"] = style_value
110
+
111
+ return DOM.create_element(tag_name, attrs, render_children(props))
112
+
113
+ element.__name__ = f"text_alignment_{tag_name}"
114
+ element.__repr__ = lambda _self: f"text_alignment_{tag_name}"
115
+ return element
116
+
117
+
118
+ def _alignment_converter_rule():
119
+ block_map = {}
120
+ from_database_format = {}
121
+
122
+ for block_type, tag_name in BLOCK_TAGS:
123
+ block_map[block_type] = {"element": _alignment_element(tag_name)}
124
+ for _type, _value, style_value, _label in ALIGNMENTS:
125
+ from_database_format[f"{tag_name}[data-dtu-text-align='{style_value}']"] = (
126
+ AlignmentHandler(block_type)
127
+ )
128
+
129
+ for block_type, wrapper in LIST_TAGS:
130
+ block_map[block_type] = {
131
+ "element": _alignment_element("li"),
132
+ "wrapper": wrapper,
133
+ }
134
+ for _type, _value, style_value, _label in ALIGNMENTS:
135
+ from_database_format[f"li[data-dtu-text-align='{style_value}']"] = (
136
+ AlignmentHandler()
137
+ )
138
+
139
+ return {
140
+ "from_database_format": from_database_format,
141
+ "to_database_format": {"block_map": block_map},
142
+ }
143
+
144
+
145
+ def register(features):
146
+ if not feature_enabled("TEXT_ALIGNMENT"):
147
+ return
148
+
149
+ # Keep parsing and re-saving old alignment content, which used block types
150
+ # and a div wrapper. Scope the selectors to ``div`` so they cannot match
151
+ # the new, alignment-preserving heading/list elements.
152
+ for type_, _value, style_value, _label in ALIGNMENTS:
153
+ register_block_feature(
154
+ features,
155
+ type_,
156
+ {},
157
+ css_selector=f"div.dtu-block_text-align[data-dtu-text-align='{style_value}']",
158
+ props={
159
+ "class": "dtu-block_text-align",
160
+ "data-dtu-text-align": style_value,
161
+ },
162
+ )
163
+
164
+ register_control_feature(
165
+ features,
166
+ "text-alignment",
167
+ js=["draftail_text_utils/js/text_alignment.js"],
168
+ css={"all": ("draftail_text_utils/css/text_alignment.css",)},
169
+ )
170
+
171
+ features.register_converter_rule(
172
+ "contentstate",
173
+ "text-alignment",
174
+ _alignment_converter_rule(),
175
+ )
176
+
177
+
178
+ def control():
179
+ if not feature_enabled("TEXT_ALIGNMENT"):
180
+ return
181
+
182
+ return control_json_script(
183
+ type_="text-alignment",
184
+ label="Align Text",
185
+ description="Change text alignment",
186
+ icon="align-left",
187
+ data=[
188
+ {
189
+ "type": type_,
190
+ "value": value,
191
+ "label": label,
192
+ "style": {"textAlign": style_value},
193
+ "icon": f"align-{value}",
194
+ }
195
+ for type_, value, style_value, label in ALIGNMENTS
196
+ ],
197
+ )
@@ -23,6 +23,7 @@
23
23
  margin: 0;
24
24
  z-index: 100;
25
25
  background: var(--w-color-surface-page, #fff);
26
+ color: var(--w-color-text-context, #1a1a1a);
26
27
  border: 1px solid var(--w-color-border-furniture, #ddd);
27
28
  border-radius: 4px;
28
29
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
@@ -38,6 +38,12 @@
38
38
  const entityType = 'TEXT_STYLE';
39
39
  const control = dt.parseControl('font-size');
40
40
 
41
+ function normaliseSize(value) {
42
+ if (value === null || value === undefined || value === '') return '';
43
+ var parsed = parseInt(value, 10);
44
+ return isNaN(parsed) ? String(value) : String(parsed);
45
+ }
46
+
41
47
  // The control component
42
48
  var FontSizeControl = class FontSizeControl extends React.Component {
43
49
  constructor(props) {
@@ -48,16 +54,19 @@
48
54
  isInputFocused: false,
49
55
  };
50
56
  this.controlRef = React.createRef();
57
+ this.replaceOnNextDigit = false;
51
58
  }
52
59
 
53
60
  // lifecycle
54
61
  componentDidMount() {
55
62
  this.syncInputValue();
56
63
  document.addEventListener('mousedown', this.handleClickOutside);
64
+ document.addEventListener('keydown', this.handleDocumentKeyDown, true);
57
65
  }
58
66
 
59
67
  componentWillUnmount() {
60
68
  document.removeEventListener('mousedown', this.handleClickOutside);
69
+ document.removeEventListener('keydown', this.handleDocumentKeyDown, true);
61
70
  }
62
71
 
63
72
  componentDidUpdate() {
@@ -65,9 +74,9 @@
65
74
  }
66
75
 
67
76
  // helpers
68
- getActiveSize() {
77
+ getActiveSize(editorState) {
69
78
  return dt.getActiveEntityData(
70
- this.props.getEditorState(),
79
+ editorState || this.props.getEditorState(),
71
80
  entityType,
72
81
  'size',
73
82
  );
@@ -79,31 +88,33 @@
79
88
  return;
80
89
  }
81
90
 
82
- var activeSize = this.getActiveSize();
83
- var newValue = activeSize || '';
91
+ var newValue = normaliseSize(this.getActiveSize());
84
92
 
85
93
  if (this.state.inputValue !== newValue) {
86
94
  this.setState({ inputValue: newValue });
87
95
  }
88
96
  }
89
97
 
90
- toggleDropdown(force) {
91
- const willBeOpen =
92
- typeof force === 'boolean' ? force : !this.state.isDropdownOpen;
93
- if (willBeOpen && !this.state.isDropdownOpen) {
94
- dt.saveSelection(this.props.getEditorState);
95
- }
96
- this.setState({ isDropdownOpen: willBeOpen });
98
+ stopEditing() {
99
+ this.replaceOnNextDigit = false;
100
+ this.setState({ isDropdownOpen: false, isInputFocused: false }, () => {
101
+ this.syncInputValue();
102
+ });
97
103
  }
98
104
 
99
105
  applyFontSize(size) {
100
106
  var editorState = dt.restoreSelection(this.props.getEditorState());
101
- var currentSize = this.getActiveSize(editorState);
107
+ var currentSize = normaliseSize(this.getActiveSize(editorState));
102
108
 
103
- if (size === currentSize) {
109
+ if (String(size) === currentSize) {
104
110
  editorState = dt.removeStyleProperty(editorState, entityType, 'size');
105
111
  this.props.onChange(editorState);
106
- this.setState({ isDropdownOpen: false, inputValue: '' });
112
+ this.replaceOnNextDigit = false;
113
+ this.setState({
114
+ isDropdownOpen: false,
115
+ isInputFocused: false,
116
+ inputValue: '',
117
+ });
107
118
  this.syncInputValue();
108
119
  return;
109
120
  }
@@ -112,82 +123,132 @@
112
123
  this.props.onChange(newContent);
113
124
 
114
125
  // Re-sync the input value
115
- this.setState({ isDropdownOpen: false, inputValue: String(size) });
126
+ this.replaceOnNextDigit = false;
127
+ this.setState({
128
+ isDropdownOpen: false,
129
+ isInputFocused: false,
130
+ inputValue: String(size),
131
+ });
116
132
  this.syncInputValue();
117
133
  }
118
134
 
119
135
  increment() {
120
- var activeSize = this.getActiveSize();
121
- var currentSize = isNaN(activeSize || this.state.inputValue || 'NaN')
122
- ? 16
123
- : parseInt(activeSize || this.state.inputValue);
136
+ var value =
137
+ this.state.isInputFocused && this.state.inputValue
138
+ ? this.state.inputValue
139
+ : this.getActiveSize();
140
+ var currentSize = isNaN(value || 'NaN') ? 16 : parseInt(value);
124
141
  this.applyFontSize(Math.min(MAX_FONT_SIZE, currentSize + FONT_SIZE_STEP));
125
142
  }
126
143
 
127
144
  decrement() {
128
- var activeSize = this.getActiveSize();
129
- var currentSize = isNaN(activeSize || this.state.inputValue || 'NaN')
130
- ? 16
131
- : parseInt(activeSize || this.state.inputValue);
145
+ var value =
146
+ this.state.isInputFocused && this.state.inputValue
147
+ ? this.state.inputValue
148
+ : this.getActiveSize();
149
+ var currentSize = isNaN(value || 'NaN') ? 16 : parseInt(value);
132
150
  this.applyFontSize(Math.max(MIN_FONT_SIZE, currentSize - FONT_SIZE_STEP));
133
151
  }
134
152
 
135
153
  // event handlers
136
154
  handleInputMouseDown = (e) => {
137
- this.toggleDropdown();
155
+ // Do not focus the input: that would blur the editor, clear Draftail's
156
+ // selection focus, and unmount the floating toolbar. Keep the editor
157
+ // focused and route keyboard input to the control instead.
158
+ e.preventDefault();
159
+ dt.saveSelection(this.props.getEditorState);
160
+ this.replaceOnNextDigit = true;
161
+ this.setState({ isDropdownOpen: true, isInputFocused: true });
138
162
  };
139
163
 
140
- handleInputFocus = () => {
141
- dt.saveSelection(this.props.getEditorState);
142
- this.setState({ isInputFocused: true });
143
- if (!this.state.isDropdownOpen) {
144
- this.setState({ isDropdownOpen: true });
164
+ handleDocumentKeyDown = (e) => {
165
+ if (!this.state.isInputFocused) return;
166
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
167
+
168
+ if (/^[0-9]$/.test(e.key)) {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ var nextValue = this.replaceOnNextDigit
172
+ ? e.key
173
+ : (this.state.inputValue || '') + e.key;
174
+ this.replaceOnNextDigit = false;
175
+ this.setState({ inputValue: nextValue });
176
+ return;
145
177
  }
146
- };
147
178
 
148
- handleInputBlur = () => {
149
- // When leaving the field, clear the focus flag and re‑sync (will clear if no style)
150
- this.setState({ isInputFocused: false }, () => {
151
- this.syncInputValue();
152
- });
153
- };
179
+ if (e.key === 'Backspace') {
180
+ e.preventDefault();
181
+ e.stopPropagation();
182
+ this.replaceOnNextDigit = false;
183
+ this.setState({
184
+ inputValue: (this.state.inputValue || '').slice(0, -1),
185
+ });
186
+ return;
187
+ }
154
188
 
155
- handleInputChange = (e) => {
156
- this.setState({ inputValue: e.target.value.replace(/[^0-9]/g, '') });
157
- };
189
+ if (e.key === 'ArrowUp') {
190
+ e.preventDefault();
191
+ e.stopPropagation();
192
+ this.increment();
193
+ return;
194
+ }
158
195
 
159
- handleInputKeyDown = (e) => {
160
- if (e.key === 'Escape') {
161
- this.toggleDropdown(false);
162
- this.syncInputValue();
196
+ if (e.key === 'ArrowDown') {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ this.decrement();
163
200
  return;
164
201
  }
165
202
 
166
- if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
203
+ if (e.key === 'Enter') {
167
204
  e.preventDefault();
168
- if (e.key === 'Enter') {
169
- var size = parseInt(this.state.inputValue, 10);
170
- if (size >= MIN_FONT_SIZE && size <= MAX_FONT_SIZE) {
171
- this.applyFontSize(size);
172
- } else {
173
- this.syncInputValue();
174
- }
175
- return;
205
+ e.stopPropagation();
206
+ var size = parseInt(this.state.inputValue, 10);
207
+ if (size >= MIN_FONT_SIZE && size <= MAX_FONT_SIZE) {
208
+ this.applyFontSize(size);
209
+ } else {
210
+ this.stopEditing();
176
211
  }
212
+ return;
213
+ }
177
214
 
178
- if (e.key === 'ArrowUp') {
179
- this.increment();
180
- return;
181
- }
215
+ if (e.key === 'Escape') {
216
+ e.preventDefault();
217
+ e.stopPropagation();
218
+ this.stopEditing();
219
+ return;
220
+ }
182
221
 
183
- this.decrement();
222
+ if (e.key === 'Tab') {
223
+ this.stopEditing();
184
224
  return;
185
225
  }
226
+
227
+ // Keep all other typing/navigation out of the editor while the control
228
+ // is capturing input.
229
+ if (
230
+ e.key.length === 1 ||
231
+ [
232
+ 'Delete',
233
+ 'Home',
234
+ 'End',
235
+ 'PageUp',
236
+ 'PageDown',
237
+ 'ArrowLeft',
238
+ 'ArrowRight',
239
+ ].includes(e.key)
240
+ ) {
241
+ e.preventDefault();
242
+ e.stopPropagation();
243
+ }
186
244
  };
187
245
 
188
246
  handleClickOutside = (event) => {
189
247
  if (dt.clickOutsideGuard(this.controlRef, event)) {
190
- this.setState({ isDropdownOpen: false });
248
+ this.replaceOnNextDigit = false;
249
+ this.setState({ isDropdownOpen: false, isInputFocused: false }, () => {
250
+ this.syncInputValue();
251
+ });
191
252
  }
192
253
  };
193
254
 
@@ -201,7 +262,8 @@
201
262
  this.applyFontSize(opt.size);
202
263
  },
203
264
  'className': 'Draftail--dtu-option Draftail--font-size-option',
204
- 'aria-selected': this.state.inputValue === opt.size,
265
+ 'aria-selected':
266
+ normaliseSize(this.state.inputValue) === String(opt.size),
205
267
  },
206
268
  opt.label,
207
269
  );
@@ -209,7 +271,6 @@
209
271
 
210
272
  // render
211
273
  render() {
212
- var activeSize = this.getActiveSize();
213
274
  var inputDisplay = this.state.inputValue;
214
275
  var isDropdownExpanded = this.state.isDropdownOpen;
215
276
  var dropdown = React.createElement(
@@ -256,14 +317,10 @@
256
317
  id: 'Draftail--font-size-input',
257
318
  className: 'Draftail--font-size-input',
258
319
  type: 'text',
259
- defaultValue: activeSize || '',
260
320
  value: inputDisplay,
261
321
  placeholder: 'Font Size',
322
+ tabIndex: -1,
262
323
  onMouseDown: this.handleInputMouseDown,
263
- onFocus: this.handleInputFocus,
264
- onBlur: this.handleInputBlur,
265
- onChange: this.handleInputChange,
266
- onKeyDown: this.handleInputKeyDown,
267
324
  }),
268
325
  );
269
326
 
@@ -12,13 +12,21 @@
12
12
 
13
13
  (function () {
14
14
  var React = window.React;
15
- var RichUtils = window.DraftJS.RichUtils;
15
+ var DraftJS = window.DraftJS;
16
16
  var ToolbarButton = window.Draftail.ToolbarButton;
17
17
  var dt = window.DraftailTextUtils;
18
18
  var data = window.draftailTextUtils || {};
19
19
  const control = dt.parseControl('text-alignment');
20
20
  const options = control.data || [];
21
21
 
22
+ // Legacy alignment was stored as a block type instead of block data.
23
+ const LEGACY_ALIGNMENTS = {
24
+ ALIGNMENT_LEFT: 'left',
25
+ ALIGNMENT_CENTER: 'center',
26
+ ALIGNMENT_RIGHT: 'right',
27
+ ALIGNMENT_JUSTIFY: 'justify',
28
+ };
29
+
22
30
  // ── Helper to find active text alignment ──
23
31
  function getActiveOption(editorState) {
24
32
  const selection = editorState.getSelection();
@@ -26,8 +34,60 @@
26
34
  .getCurrentContent()
27
35
  .getBlockForKey(selection.getStartKey());
28
36
  if (!block) return null;
29
- const blockType = block.getType();
30
- return options.find((opt) => opt.type === blockType) || null;
37
+ const alignment =
38
+ block.getData().get('alignment') || LEGACY_ALIGNMENTS[block.getType()];
39
+ return options.find((opt) => opt.value === alignment) || null;
40
+ }
41
+
42
+ function setAlignment(editorState, alignment) {
43
+ const selection = editorState.getSelection();
44
+ const startKey = selection.getStartKey();
45
+ const endKey = selection.getEndKey();
46
+ let content = editorState.getCurrentContent();
47
+ let blockKey = startKey;
48
+
49
+ while (blockKey) {
50
+ let block = content.getBlockForKey(blockKey);
51
+ const blockSelection = selection.merge({
52
+ anchorKey: blockKey,
53
+ anchorOffset: 0,
54
+ focusKey: blockKey,
55
+ focusOffset: block.getLength(),
56
+ isBackward: false,
57
+ });
58
+
59
+ // Migrate legacy ALIGNMENT_* blocks to a regular paragraph plus block
60
+ // data, so the new control can update them.
61
+ if (LEGACY_ALIGNMENTS[block.getType()]) {
62
+ content = DraftJS.Modifier.setBlockType(
63
+ content,
64
+ blockSelection,
65
+ 'unstyled',
66
+ );
67
+ block = content.getBlockForKey(blockKey);
68
+ }
69
+
70
+ let blockData = block.getData();
71
+ if (alignment) {
72
+ blockData = blockData.set('alignment', alignment);
73
+ } else {
74
+ blockData = blockData.delete('alignment');
75
+ }
76
+
77
+ content = DraftJS.Modifier.setBlockData(
78
+ content,
79
+ blockSelection,
80
+ blockData,
81
+ );
82
+
83
+ if (blockKey === endKey) break;
84
+ blockKey = content.getKeyAfter(blockKey);
85
+ }
86
+
87
+ return DraftJS.EditorState.forceSelection(
88
+ DraftJS.EditorState.push(editorState, content, 'change-block-data'),
89
+ selection,
90
+ );
31
91
  }
32
92
 
33
93
  class TextAlignmentControl extends React.Component {
@@ -65,8 +125,10 @@
65
125
  handleOptionClick = (opt) => {
66
126
  const { getEditorState, onChange } = this.props;
67
127
  const editorState = getEditorState();
68
- const newState = RichUtils.toggleBlockType(editorState, opt.type);
69
- onChange(newState);
128
+ const activeOption = getActiveOption(editorState);
129
+ const removing = activeOption && activeOption.type === opt.type;
130
+
131
+ onChange(setAlignment(editorState, removing ? null : opt.value));
70
132
  this.setState({ isOpen: false });
71
133
  };
72
134
 
@@ -127,6 +189,35 @@
127
189
  }
128
190
  }
129
191
 
192
+ function blockStyleFn(block) {
193
+ const alignment = block.getData().get('alignment');
194
+ if (!alignment) return null;
195
+ return `Draftail-block--ALIGNMENT_${alignment.toUpperCase()}`;
196
+ }
197
+
198
+ // Add alignment classes to the editor blocks without changing their type.
199
+ if (window.draftail && window.draftail.initEditor) {
200
+ var originalInitEditor = window.draftail.initEditor;
201
+ if (!originalInitEditor.__dtuTextAlignment) {
202
+ var patchedInitEditor = function (selector, options, script) {
203
+ options = options || {};
204
+ options.plugins = (options.plugins || []).concat([
205
+ {
206
+ type: 'dtu-text-alignment',
207
+ blockStyleFn: blockStyleFn,
208
+ },
209
+ ]);
210
+ return Reflect.apply(originalInitEditor, window.draftail, [
211
+ selector,
212
+ options,
213
+ script,
214
+ ]);
215
+ };
216
+ patchedInitEditor.__dtuTextAlignment = true;
217
+ window.draftail.initEditor = patchedInitEditor;
218
+ }
219
+ }
220
+
130
221
  window.draftail.registerPlugin(
131
222
  {
132
223
  type: control.type,
@@ -58,8 +58,10 @@ def register_text_style_feature(features):
58
58
  text_style.register(features)
59
59
 
60
60
 
61
- @hooks.register("register_rich_text_features")
61
+ @hooks.register("register_rich_text_features", order=100)
62
62
  def register_text_alignment_feature(features):
63
+ # Run after Wagtail's built-in block features so the alignment converter
64
+ # rule is applied after (and can therefore override) their block maps.
63
65
  text_alignment.register(features)
64
66
 
65
67
 
@@ -1 +0,0 @@
1
- __version__ = "3.0.2"
@@ -1,70 +0,0 @@
1
- """
2
- Draftail rich-text feature: Text alignment.
3
-
4
- Registers:
5
- - Four ``BlockFeature``s per alignment value.
6
- - A ``"text-alignment"`` ControlFeature for the toolbar button group.
7
- """
8
-
9
- from django.utils.translation import gettext_lazy as _
10
-
11
- from draftail_text_utils.conf import feature_enabled
12
-
13
- from .base import (
14
- control_json_script,
15
- register_block_feature,
16
- register_control_feature,
17
- )
18
-
19
-
20
- ALIGNMENTS = [
21
- ("ALIGNMENT_LEFT", "left", "start", _("Left")),
22
- ("ALIGNMENT_CENTER", "center", "center", _("Center")),
23
- ("ALIGNMENT_RIGHT", "right", "end", _("Right")),
24
- ("ALIGNMENT_JUSTIFY", "justify", "justify", _("Justify")),
25
- ]
26
-
27
-
28
- def register(features):
29
- if not feature_enabled("TEXT_ALIGNMENT"):
30
- return
31
-
32
- for type_, _value, styleValue, _label in ALIGNMENTS:
33
- register_block_feature(
34
- features,
35
- type_,
36
- {},
37
- css_selector=f".dtu-block_text-align[data-dtu-text-align='{styleValue}']",
38
- props={
39
- "class": "dtu-block_text-align",
40
- "data-dtu-text-align": styleValue,
41
- },
42
- )
43
-
44
- register_control_feature(
45
- features,
46
- "text-alignment",
47
- js=["draftail_text_utils/js/text_alignment.js"],
48
- css={"all": ("draftail_text_utils/css/text_alignment.css",)},
49
- )
50
-
51
-
52
- def control():
53
- if not feature_enabled("TEXT_ALIGNMENT"):
54
- return
55
-
56
- return control_json_script(
57
- type_="text-alignment",
58
- label="Align Text",
59
- description="Change text alignment",
60
- icon="align-left",
61
- data=[
62
- {
63
- "type": type_,
64
- "label": label,
65
- "style": {"textAlign": styleValue},
66
- "icon": f"align-{value}",
67
- }
68
- for type_, value, styleValue, label in ALIGNMENTS
69
- ],
70
- )