semantic-forms 5.1.12 → 5.2.1

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.
@@ -132,6 +132,54 @@
132
132
  </dl>
133
133
  </form>
134
134
 
135
+ <h5>Add custom keyboard shortcuts</h5>
136
+
137
+ <p>Add keyboard shortcuts on specific inputs to focus them by adding a <code>data-focus-key</code> attribute and optional <code>data-focus-modifier</code> attribute.</p>
138
+
139
+ <pre><code class='language-html' lang='html'><escape>
140
+ <form class="semanticForms" method="post">
141
+ <dl>
142
+ <dt><label for="focus-1">Focus me!</label></dt>
143
+ <dd><input id="focus-1" name="focus-1" type="text" data-focus-key="Y"></dd>
144
+
145
+ <!-- change the modifier -->
146
+ <dt><label for="focus-2">Focus me!</label></dt>
147
+ <dd><input id="focus-2" name="focus-2" type="text" data-focus-key="Y" data-focus-modifier="alt"></dd>
148
+
149
+ <!-- specify modifier by OS -->
150
+ <dt><label for="focus-2">Focus me!</label></dt>
151
+ <dd><input id="focus-2" name="focus-2" type="text" data-focus-key="Z" data-focus-modifier-win="alt" data-focus-modifier-linux="meta" data-focus-modifier-mac="cmd"></dd>
152
+ </dl>
153
+ </form>
154
+ </escape></code></pre>
155
+
156
+ <form class="semanticForms" method="post">
157
+ <dl>
158
+ <dt><label for="focus-1">Focus me!</label></dt>
159
+ <dd><input id="focus-1" name="focus-1" type="text" data-focus-key="Y"></dd>
160
+
161
+ <!-- change the modifier -->
162
+ <dt><label for="focus-2">Focus me!</label></dt>
163
+ <dd><input id="focus-2" name="focus-2" type="text" data-focus-key="Y" data-focus-modifier="alt"></dd>
164
+
165
+ <!-- specify modifier by OS -->
166
+ <dt><label for="focus-3">Focus me!</label></dt>
167
+ <dd><input id="focus-3" name="focus-3" type="text" data-focus-key="Z" data-focus-modifier-win="alt" data-focus-modifier-linux="meta" data-focus-modifier-max="cmd"></dd>
168
+ </dl>
169
+ </form>
170
+
171
+ <ul>
172
+ <li><p>The focus key should be a single character, and is not case-sensitive. If multiple characters are detected, only the first one will be used. Special characters (!, @, #, etc.) will require <kbd>Shift</kbd> to be pressed with the keyboard shortcut.</p></li>
173
+ <li>
174
+ <p>The modifier key value may be "metactrl," "ctrl," "cmd," "meta," or "alt." The default value is "metactrl" and will automatically be applied if the <code>data-focus-modifier</code> attribute is not present.</p>
175
+ <ul>
176
+ <li>Some combinations are unable to be used due to conflicts with browser and OS keyboard shortcuts. See <a href="https://rooseveltframework.org/docs/semantic-forms/latest/reservedKeyboardShortcuts">reserved keyboard shortcuts</a> for a full overview.</li>
177
+ <li><p>OS-specific modifier keys can be set with <code>data-focus-modifier-win</code> (Windows), <code>data-focus-modifier-linux</code> (Linux) and <code>data-focus-modifier-mac</code> (Mac).</p></li>
178
+ <li><p>Due to a limited range of available shortcuts, the "Start" (win) key on Windows will be replaced with "Ctrl."</p></li>
179
+ </ul>
180
+ </li>
181
+ </ul>
182
+
135
183
  <h5 id="custominputfields">Customizing input fields</h5>
136
184
 
137
185
  <p>By default, input fields will display an X icon to clear the text and password fields will also display a show/hide toggle for displaying the password in plain text. You can customize this behavior as follows:</p>
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "url": "https://github.com/rooseveltframework/semantic-forms/graphs/contributors"
9
9
  }
10
10
  ],
11
- "version": "5.1.12",
11
+ "version": "5.2.1",
12
12
  "files": [
13
13
  "dist",
14
14
  "semanticForms.scss",
@@ -21,8 +21,8 @@
21
21
  "readmeFilename": "README.md",
22
22
  "devDependencies": {
23
23
  "@jsdevtools/coverage-istanbul-loader": "3.0.5",
24
- "@playwright/test": "1.56.0",
25
- "eslint": "9.37.0",
24
+ "@playwright/test": "1.56.1",
25
+ "eslint": "9.38.0",
26
26
  "eslint-plugin-html": "8.1.3",
27
27
  "express": "5.1.0",
28
28
  "nodemon": "3.1.10",
package/semanticForms.js CHANGED
@@ -11,9 +11,84 @@ const semanticForms = () => {
11
11
 
12
12
  const nodeNameLookup = ['TEXTAREA', 'SELECT']
13
13
  const inputTypeLookup = ['checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'image', 'month', 'number', 'password', 'radio', 'range', 'search', 'tel', 'text', 'time', 'url', 'week']
14
+ const keyboardShortcuts = []
15
+
16
+ // custom keyboard shortcut listener
17
+ document.addEventListener('keydown', (e) => {
18
+ const specialCharMap = {
19
+ Minus: '-',
20
+ Equal: '=',
21
+ BracketLeft: '[',
22
+ BracketRight: ']',
23
+ Backslash: '\\',
24
+ Semicolon: ';',
25
+ Quote: '\'',
26
+ Comma: ',',
27
+ Period: '.',
28
+ Slash: '/',
29
+ Backquote: '`'
30
+ }
31
+
32
+ const shiftSpecialCharMap = {
33
+ 1: '!',
34
+ 2: '@',
35
+ 3: '#',
36
+ 4: '$',
37
+ 5: '%',
38
+ 6: '^',
39
+ 7: '&',
40
+ 8: '*',
41
+ 9: '(',
42
+ 0: ')',
43
+ Minus: '_',
44
+ Equal: '+',
45
+ BracketLeft: '{',
46
+ BracketRight: '}',
47
+ Backslash: '|',
48
+ Semicolon: ':',
49
+ Quote: '"',
50
+ Comma: '<',
51
+ Period: '>',
52
+ Slash: '?',
53
+ Backquote: '~'
54
+ }
55
+
56
+ const shortcut = keyboardShortcuts.find(shortcut => {
57
+ let matchesKey = false
58
+ if (e.altKey && !e.shiftKey) {
59
+ // mac adjusts the key value if altKey is pressed
60
+ matchesKey = 'Key' + shortcut.key.toUpperCase() === e.code ||
61
+ 'Digit' + shortcut.key.toUpperCase() === e.code ||
62
+ shortcut.key === e.key ||
63
+ specialCharMap[e.code] === shortcut.key
64
+ } else if (e.shiftKey) {
65
+ // check shift special character map
66
+ const code = e.code.replace(/Key|Digit/, '')
67
+ matchesKey = (shiftSpecialCharMap[code] || shiftSpecialCharMap[e.key]) &&
68
+ (shiftSpecialCharMap[code] === shortcut.key || shiftSpecialCharMap[e.key] === shortcut.key)
69
+ } else {
70
+ matchesKey = shortcut.key.toUpperCase() === e.key.toUpperCase()
71
+ }
72
+ if (!matchesKey) return false
73
+
74
+ let matchesModifier
75
+ if (shortcut.modifier) {
76
+ if (shortcut.modifier === shortcut.defaultModifier) matchesModifier = shortcut.os === 'windows' || shortcut.os === 'linux' ? e.ctrlKey : e.metaKey
77
+ if (shortcut.modifier === 'meta') matchesModifier = e.metaKey
78
+ if (shortcut.modifier === 'alt') matchesModifier = e.altKey
79
+ if (shortcut.modifier === 'ctrl') matchesModifier = e.ctrlKey
80
+ }
81
+ return matchesModifier
82
+ })
83
+
84
+ if (shortcut) {
85
+ e.preventDefault()
86
+ shortcut.input.focus()
87
+ }
88
+ })
14
89
 
15
90
  // progressively enhance form elements that have the semanticForms class
16
- const forms = document.querySelectorAll('form.semanticForms:not(.semanticFormsActive)')
91
+ const forms = document.querySelectorAll('form.semanticForms:not(.semanticFormsActive), table.semanticForms:not(.semanticFormsActive)')
17
92
 
18
93
  for (const form of forms) {
19
94
  form.classList.add('semanticFormsActive')
@@ -45,6 +120,10 @@ const semanticForms = () => {
45
120
  input.classList.add('semanticform')
46
121
 
47
122
  // #region create labels
123
+
124
+ const newLabel = document.createElement('label')
125
+ newLabel.className = 'floatLabelFormAnimatedLabel'
126
+
48
127
  if (type === 'checkbox' || type === 'radio') {
49
128
  // recursively find <dd> element
50
129
  let dd = input.parentNode
@@ -56,9 +135,6 @@ const semanticForms = () => {
56
135
  }
57
136
 
58
137
  if (dd.firstChild.nodeName !== 'LABEL') {
59
- const newLabel = document.createElement('label')
60
- newLabel.className = 'floatLabelFormAnimatedLabel'
61
-
62
138
  if (type === 'checkbox' && input.parentNode.nodeName === 'DD') {
63
139
  newLabel.setAttribute('for', input.id)
64
140
  input.parentNode.classList.add('singleCheckbox')
@@ -99,10 +175,7 @@ const semanticForms = () => {
99
175
  div.append(label.closest('dt'), dd)
100
176
  dl.append(div)
101
177
  } else {
102
- const newLabel = document.createElement('label')
103
178
  newLabel.setAttribute('for', input.id)
104
- newLabel.className = 'floatLabelFormAnimatedLabel'
105
-
106
179
  newLabel.innerHTML = label.innerHTML
107
180
 
108
181
  if (input.hasAttribute('title') && label.getAttribute('data-show-help-icon') !== null && !label.querySelector('span.help')) {
@@ -121,6 +194,97 @@ const semanticForms = () => {
121
194
  }
122
195
  // #endregion
123
196
 
197
+ // #region keyboard shortcuts
198
+ if (input.getAttribute('data-focus-key') !== null) {
199
+ function handleKeyboardShortcut () {
200
+ const os = getOS()
201
+ // this is the custom keyword for meta on linux/mac, ctrl on windows
202
+ const defaultModifier = 'metactrl'
203
+
204
+ // get focus key value
205
+ let focusKey = input.getAttribute('data-focus-key')
206
+ if (focusKey.length > 1) {
207
+ console.error(`Provided focus key "${focusKey}" is more than one character. Using first character only.`)
208
+ focusKey = focusKey.toString()[0]
209
+ }
210
+
211
+ // get focus modifier value
212
+ let modifierSymbol
213
+ let modifierKey = defaultModifier
214
+ const modifierAttr = {
215
+ default: input.getAttribute('data-focus-modifier') || defaultModifier,
216
+ linux: input.getAttribute('data-focus-modifier-linux'),
217
+ mac: input.getAttribute('data-focus-modifier-mac'),
218
+ windows: input.getAttribute('data-focus-modifier-win')
219
+ }
220
+ if (os && modifierAttr[os]) {
221
+ // a specific modifier key has been set by the user
222
+ modifierKey = modifierAttr[os]
223
+ } else {
224
+ modifierKey = modifierAttr.default
225
+ }
226
+
227
+ // validate passed in modifier
228
+ const recognizedModifiers = ['ctrl', 'alt', 'opt', 'meta', 'cmd', defaultModifier]
229
+ if (!recognizedModifiers.includes(modifierKey)) {
230
+ console.error(`Received an unrecognized modifier, "${modifierKey}," defaulting to "${defaultModifier}."`, input)
231
+ modifierKey = defaultModifier
232
+ }
233
+
234
+ // retrieve modifier symbol
235
+ if (['alt', 'opt'].includes(modifierKey)) {
236
+ modifierSymbol = os === 'mac' ? '⌥' : '⎇'
237
+ } else if (['meta', 'win', 'cmd'].includes(modifierKey) || (modifierKey === defaultModifier && os === 'mac')) {
238
+ if (os === 'mac') {
239
+ modifierSymbol = '⌘'
240
+ } else if (os === 'linux') {
241
+ modifierSymbol = '◆'
242
+ } else {
243
+ modifierSymbol = '⊞'
244
+ }
245
+ } else if (modifierKey === 'ctrl' || (modifierKey === defaultModifier && (os === 'windows' || os === 'linux'))) {
246
+ if (os === 'mac') {
247
+ modifierSymbol = '⌃'
248
+ } else {
249
+ modifierSymbol = 'Ctrl'
250
+ }
251
+ }
252
+
253
+ // add the shortcut to the cached array, if not a duplicate
254
+ if (keyboardShortcuts.some(shortcut => shortcut.key === focusKey && shortcut.modifier === modifierKey)) {
255
+ console.error(`Duplicate keyboard shortcut "${modifierKey} + ${focusKey}" detected. Only the first input will be focusable using this keyboard shortcut.`, input)
256
+ } else {
257
+ keyboardShortcuts.push({
258
+ key: focusKey,
259
+ modifier: modifierKey,
260
+ input,
261
+ os,
262
+ defaultModifier
263
+ })
264
+ }
265
+
266
+ // set the shortcut indicator/title
267
+ if (input.nodeName === 'TEXTAREA' || input.type === 'text' || input.type === 'number') {
268
+ // create focus indicator for valid inputs
269
+ const indicator = document.createElement('span')
270
+ indicator.classList.add('focus-key')
271
+ indicator.innerHTML = `<kbd>${modifierSymbol} ${focusKey.toUpperCase()}</kbd>`
272
+ insertAfter(indicator, newLabel)
273
+ } else {
274
+ // update the input title
275
+ if (input.getAttribute('title')) {
276
+ input.setAttribute('title', input.getAttribute('title') + ` (${modifierSymbol} + ${focusKey})`)
277
+ } else {
278
+ input.setAttribute('title', `Focus with ${modifierSymbol} + ${focusKey}`)
279
+ }
280
+ }
281
+ }
282
+
283
+ // placed in a function so that it may exit while still completing other semantic-form enhancements
284
+ handleKeyboardShortcut()
285
+ }
286
+ // #endregion
287
+
124
288
  // #region standard inputs
125
289
  // check for auto-grow attribute on textareas
126
290
  if (input.getAttribute('data-auto-grow') !== null) {
@@ -133,7 +297,6 @@ const semanticForms = () => {
133
297
  newInput.setAttribute('data-auto-grow', '')
134
298
  input.replaceWith(newInput)
135
299
  input = newInput
136
-
137
300
  }
138
301
 
139
302
  if (input.nodeName === 'TEXTAREA') {
@@ -194,9 +357,9 @@ const semanticForms = () => {
194
357
  dd.classList.remove(match)
195
358
  div.classList.add(match)
196
359
  }
197
-
360
+
198
361
  // check for max-content attribute
199
- // this may be removed once fully supported in Firefox and Safari: https://caniuse.com/wf-field-sizing
362
+ // this may be removed once fully supported in Firefox and Safari: https://caniuse.com/wf-field-sizing
200
363
  if (input.getAttribute('data-max-content') !== null) {
201
364
  if (!('fieldSizing' in document.createElement('input').style)) {
202
365
  const adjustWidth = () => {
@@ -342,7 +505,7 @@ const semanticForms = () => {
342
505
  }
343
506
 
344
507
  // progressively enhance textarea for Firefox and Safari
345
- // this may be removed once fully supported in Firefox and Safari: https://caniuse.com/wf-field-sizing
508
+ // this may be removed once fully supported in Firefox and Safari: https://caniuse.com/wf-field-sizing
346
509
  if (input.getAttribute('data-auto-grow') !== null) {
347
510
  const adjustHeight = () => {
348
511
  if (input.value.length) {
@@ -380,6 +543,25 @@ const semanticForms = () => {
380
543
  else referenceNode.parentNode.appendChild(newNode)
381
544
  }
382
545
 
546
+ /**
547
+ * Uses the navigator to best determine the clients operating system.
548
+ * @returns Operating system string (`mac`, `windows`, `linux`)
549
+ */
550
+ function getOS () {
551
+ const userAgent = window.navigator.userAgent
552
+ const platform = window.navigator.platform
553
+ let os = null
554
+
555
+ if (platform.includes('Win')) {
556
+ os = 'windows'
557
+ } else if (platform.includes('Mac') || /iPhone|iPad|iPod/.test(userAgent)) {
558
+ os = 'mac'
559
+ } else if (platform.includes('Linux') || /Android/.test(userAgent)) {
560
+ os = 'linux'
561
+ }
562
+ return os
563
+ }
564
+
383
565
  // handle undo/redo
384
566
  let lastFocusedInput
385
567
  let lastClearFieldPressed