• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divio / django-cms / #30138

19 Nov 2025 07:08AM UTC coverage: 89.921% (+12.2%) from 77.7%
#30138

push

travis-ci

web-flow
Merge 47ea1d265 into e10689d2d

1337 of 2148 branches covered (62.24%)

9207 of 10239 relevant lines covered (89.92%)

11.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.82
/cms/static/cms/js/modules/shortcuts/placeholders.js
1
import { trap, untrap } from '../trap.js';
2
import keyboard from '../keyboard.js';
3

4

5
const queryAll = (selector, root = document) => Array.from(root.querySelectorAll(selector));
1!
6
const query = (selector, root = document) => root?.querySelector(selector);
1!
7
const triggerPointerUp = el => { if (el) el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); };
1!
8
const triggerClick = el => { if (el) el.click(); };
1!
9
const setTabIndex = (elements, value) => elements.forEach(el => el.setAttribute('tabindex', value));
1✔
10
const removeTabIndex = elements => elements.forEach(el => el.removeAttribute('tabindex'));
1✔
11
const focusElement = el => { if (el) el.focus(); };
1!
12

13
/**
14
 * Binds shortcuts:
15
 * [esc] go back to global "cms" context
16
 * [enter] when focusing the placeholder - go to it's first plugin
17
 * [+] / [a] add plugin to placeholder
18
 * [x] expand all / collapse all
19
 * [s] open settings menu for current placeholder
20
 *
21
 * @function bindPlaceholderKeys
22
 * @private
23
 */
24
var bindPlaceholderKeys = function () {
1✔
25
    keyboard.setContext('placeholders');
1✔
26
    keyboard.bind('escape', function () {
1✔
27
        queryAll('.cms-structure .cms-dragarea').forEach(el => el.removeAttribute('tabindex'));
×
28
        untrap(query('.cms-structure-content'));
×
29
        focusElement(document.documentElement);
×
30
        keyboard.setContext('cms');
×
31
    });
32

33
    keyboard.bind('enter', function () {
1✔
34
        keyboard.setContext('plugins');
×
35
        const area = document.activeElement.closest('.cms-dragarea');
×
36
        if (!area) return;
×
37
        const plugins = queryAll('.cms-dragitem', area);
×
38
        if (!plugins.length) return;
×
39
        setTabIndex(plugins, '0');
×
40
        focusElement(plugins[0]);
×
41
        trap(area);
×
42
        keyboard.setContext('plugins');
×
43
    });
44

45
    keyboard.bind(['+', 'a'], function () {
1✔
46
        const area = document.activeElement.closest('.cms-dragarea');
×
47
        if (!area) return;
×
48
        const addBtn = query('.cms-submenu-add', area);
×
49
        triggerPointerUp(addBtn);
×
50
    });
51

52
    keyboard.bind('x', function () {
1✔
53
        const area = document.activeElement.closest('.cms-dragarea');
×
54
        if (!area) return;
×
55
        const toggler = query('.cms-dragbar-toggler a', area);
×
56
        triggerClick(toggler);
×
57
    });
58

59
    keyboard.bind(['!', 's'], function () {
1✔
60
        const area = document.activeElement.closest('.cms-dragarea');
×
61
        if (!area) return;
×
62
        const settingsBtn = query('.cms-submenu-settings', area);
×
63
        triggerPointerUp(settingsBtn);
×
64
        keyboard.setContext('placeholder-actions');
×
65
        const submenuItem = query('.cms-submenu-item a', area);
×
66
        focusElement(submenuItem);
×
67
        const dropdownInner = query('.cms-dropdown-inner', area);
×
68
        trap(dropdownInner);
×
69
    });
70
};
71

72

73
/**
74
 * Binds shortcuts:
75
 * [esc] go back to "placeholders" context
76
 *
77
 * @function bindPlaceholderActionKeys
78
 * @private
79
 */
80
var bindPlaceholderActionKeys = function () {
1✔
81
    keyboard.setContext('placeholder-actions');
1✔
82
    keyboard.bind('escape', function () {
1✔
83
        const dropdown = query('.cms-dropdown-inner');
×
84
        if (!dropdown) return;
×
85
        const area = dropdown.closest('.cms-dragarea');
×
86
        const settingsBtn = query('.cms-submenu-settings', area);
×
87
        // Helper functions for DOM operations
88
        untrap(dropdown);
×
89
        focusElement(area);
×
90
        keyboard.setContext('placeholders');
×
91
    });
92
};
93

94
/**
95
 * Binds shortcuts:
96
 * [esc] go back to "placeholders" context
97
 * [e] edit plugin
98
 * [+] / [a] add child plugin
99
 * [x] expand / collapse plugin
100
 * [s] open settings menu
101
 *
102
 * @function bindPluginKeys
103
 * @private
104
 */
105
var bindPluginKeys = function () {
1✔
106
    keyboard.setContext('plugins');
1✔
107
    keyboard.bind('escape', function () {
1✔
108
        const plugin = document.activeElement.closest('.cms-dragitem');
×
109
        if (!plugin) return;
×
110
        const area = plugin.closest('.cms-dragarea');
×
111
        removeTabIndex(queryAll('.cms-dragitem'));
×
112
        untrap(area);
×
113
        focusElement(area);
×
114
        keyboard.setContext('placeholders');
×
115
    });
116

117
    keyboard.bind('e', function () {
1✔
118
        const plugin = document.activeElement.closest('.cms-dragitem');
×
119
        if (!plugin) return;
×
120
        const editBtn = query('.cms-submenu-edit', plugin);
×
121
        triggerClick(editBtn);
×
122
    });
123
    keyboard.bind(['+', 'a'], function () {
1✔
124
        const plugin = document.activeElement.closest('.cms-dragitem');
×
125
        if (!plugin) return;
×
126
        const addBtn = query('.cms-submenu-add', plugin);
×
127
        triggerPointerUp(addBtn);
×
128
    });
129
    keyboard.bind('x', function () {
1✔
130
        const plugin = document.activeElement.closest('.cms-dragitem');
×
131
        if (!plugin) return;
×
132
        const textBtn = query('.cms-dragitem-text', plugin);
×
133
        triggerClick(textBtn);
×
134
    });
135
    keyboard.bind(['!', 's'], function () {
1✔
136
        const plugin = document.activeElement.closest('.cms-dragitem');
×
137
        if (!plugin) return;
×
138
        const settingsBtn = query('.cms-submenu-settings', plugin);
×
139
        triggerPointerUp(settingsBtn);
×
140
        keyboard.setContext('plugin-actions');
×
141
        const submenuItem = query('.cms-submenu-item a', plugin);
×
142
        focusElement(submenuItem);
×
143
        const dropdownInner = query('.cms-dropdown-inner', plugin);
×
144
        trap(dropdownInner);
×
145
    });
146
};
147

148
/**
149
 * Binds shortcuts:
150
 * [esc] go back to "plugins" context
151
 *
152
 * @function bindPluginActionKeys
153
 * @private
154
 */
155
var bindPluginActionKeys = function () {
1✔
156
    keyboard.setContext('plugin-actions');
1✔
157
    keyboard.bind('escape', function () {
1✔
158
        const dropdown = query('.cms-dropdown-inner');
×
159
        if (!dropdown) return;
×
160
        const plugin = dropdown.closest('.cms-dragitem');
×
161
        const settingsBtn = query('.cms-submenu-settings', plugin);
×
162
        triggerPointerUp(settingsBtn);
×
163
        untrap(dropdown);
×
164
        focusElement(plugin);
×
165
        keyboard.setContext('plugins');
×
166
    });
167
};
168

169
/**
170
 * Binds [f p] / [alt+p] shortcuts to focus first placeholder.
171
 * Only works in structure mode.
172
 *
173
 * @function initPlaceholders
174
 * @public
175
 */
176
export default function initPlaceholders() {
177
    const data = CMS.config.lang.shortcutAreas[1].shortcuts.placeholders;
1✔
178

179
    bindPlaceholderKeys();
1✔
180
    bindPlaceholderActionKeys();
1✔
181
    bindPluginKeys();
1✔
182
    bindPluginActionKeys();
1✔
183

184
    keyboard.setContext('cms');
1✔
185

186
    keyboard.bind(data.shortcut.split(' / '), function () {
1✔
187
        if (CMS.settings.mode !== 'structure') {
×
188
            return;
×
189
        }
190
        const dragareas = queryAll('.cms-structure .cms-dragarea');
×
191
        setTabIndex(dragareas, '0');
×
192
        focusElement(dragareas[0]);
×
193
        trap(query('.cms-structure-content'));
×
194
        keyboard.setContext('placeholders');
×
195
    });
196
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc