@peter.naydenov/shortcuts 3.1.3 → 3.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.
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ function _registerShortcutEvents ( dependencies, pluginState ) {
4
+ const
5
+ { regex, _defaults, ev } = dependencies
6
+ , {
7
+ currentContext : { name: contextName }
8
+ , shortcuts
9
+ , callbacks
10
+ } = pluginState
11
+ ;
12
+ let watch=[], define=[], action=[];
13
+ if ( contextName == null ) return false
14
+ Object.entries ( shortcuts[contextName] ).forEach ( ([shortcutName, list ]) => {
15
+ let isFormEv = regex.test ( shortcutName );
16
+ if ( !isFormEv ) return
17
+ if ( shortcutName === 'FORM:WATCH' ) watch = list
18
+ if ( shortcutName === 'FORM:DEFINE' ) define = list
19
+ if ( shortcutName === 'FORM:ACTION' ) action = list
20
+ })
21
+
22
+ if ( action.length === 0 ) return false
23
+
24
+ let setTypes = new Set ();
25
+ if ( define.length === 0 ) define = [ _defaults.define ]
26
+ if ( watch.length === 0 ) watch = [ _defaults.watch ]
27
+ let watchList = watch.map ( el => el() )
28
+ .reduce ( ( res, el) => {
29
+ res.push ( el )
30
+ return res
31
+ }, [])
32
+ pluginState.watchList = document.querySelectorAll ( watchList )
33
+ pluginState.watchList.forEach ( el => setTypes.add (define[0](el)) )
34
+
35
+ pluginState.typeFn = define[0] ? define[0] : _defaults.define
36
+ action.forEach ( act => {
37
+
38
+ if ( !(act instanceof Function)) {
39
+ console.warn ( `Warning: The 'form:action' should be a function.` )
40
+ return false
41
+ }
42
+ let list = act ()
43
+ if ( !(list instanceof Array) ) {
44
+ console.warn ( `Warning: The 'form:action' function should RETURN an array.` )
45
+ return false
46
+ }
47
+ act().forEach ( ({fn, type, timing, wait=0 }) => {
48
+ if ( setTypes.has ( type) && fn instanceof Function ) {
49
+ let key = `${type}/${timing}`
50
+ const hasProperty = callbacks.hasOwnProperty ( key );
51
+ hasProperty ?
52
+ callbacks[key].push ( fn ) :
53
+ callbacks[key] = [ fn ]
54
+ if ( !hasProperty ) {
55
+ ev.on ( key, ( props, callbacks ) => { // Register the 'type/timing' as an event
56
+ callbacks.forEach ( cb => { if ( cb instanceof Function ) cb ( props ) })
57
+ })
58
+ }
59
+ } // if function
60
+ if ( timing === 'instant' ) pluginState.wait[`${type}`] = wait
61
+ }) // for each act
62
+ }) // for each action
63
+ if ( Object.keys(pluginState.callbacks).length > 0 ) return true
64
+ else return false
65
+ } // _registerShortcutEvents func.
66
+
67
+
68
+
69
+ export default _registerShortcutEvents
70
+
71
+
@@ -0,0 +1,84 @@
1
+ 'use strict'
2
+
3
+ // import all plugin files here
4
+ import _listenDOM from './_listenDOM.js'
5
+ import _normalizeShortcutName from './_normalizeShortcutName.js'
6
+ import _registerShortcutEvents from './_registerShortcutEvents.js'
7
+ import _defaults from './_defaults.js'
8
+
9
+
10
+
11
+ function pluginForm ( dependencies, state, options ) {
12
+ /**
13
+ * 'form: watch' - A function. Should return a string. Define a selection that will be watched for changes. example: 'input, select.color, textarea, #name'
14
+ * 'form: define' - A function that receives every watched element. Should return text value that represents the type of the
15
+ * element according custom specification. Types could be specific for every single form.
16
+ * 'form:action' - List of Callback objects.
17
+ * Callback definition object:
18
+ * {
19
+ * fn: function to be called
20
+ * type: fn should be executed on type of the element
21
+ * timing: 'in' | 'out' | 'instant' - when to execute the function
22
+ * wait: time in milliseconds to wait before executing the function again. Works only with mode 'instant'.
23
+ * }
24
+ */
25
+
26
+ let
27
+ { currentContext, shortcuts } = state
28
+ , { inAPI } = dependencies
29
+ , deps = {
30
+ ev: dependencies.ev
31
+ , mainDependencies : dependencies
32
+ , regex : /FORM\s*\:/i
33
+ , _defaults
34
+ }
35
+ , pluginState = {
36
+ currentContext
37
+ , shortcuts
38
+ , callbacks : {} // Functions callbacks arranged by 'type/timing' : [ callback, ...otherCallbacks ]
39
+ , typeFn : '' // Type definition function
40
+ , watchList : [] // list of watched elements
41
+ , wait : {} // wait time for 'instant' mode
42
+ } // pluginState
43
+ ;
44
+ function resetState () {
45
+ pluginState.callbacks = {}
46
+ pluginState.typeFn = ''
47
+ pluginState.watchList = []
48
+ pluginState.wait = {}
49
+ } // resetState func.
50
+
51
+ // Read shortcuts names from all context entities and normalize entries related to the plugin
52
+ inAPI._normalizeWithPlugins ( _normalizeShortcutName )
53
+ let formListener = _listenDOM ( deps, pluginState );
54
+
55
+ if ( currentContext.name ) {
56
+ let hasFormShortcuts = _registerShortcutEvents ( deps, pluginState )
57
+ if ( hasFormShortcuts ) formListener.start ()
58
+ }
59
+
60
+ let pluginAPI = {
61
+ getPrefix : ( ) => 'form'
62
+ , shortcutName : key => { // Format a key string according plugin needs
63
+ return _normalizeShortcutName ( key )
64
+ }
65
+ , contextChange : ( ) => {
66
+ resetState ()
67
+ let hasFormShortcuts = _registerShortcutEvents ( deps, pluginState )
68
+ if ( hasFormShortcuts ) formListener.start ()
69
+ else formListener.stop ()
70
+ }
71
+ , mute : () => formListener.stop ()
72
+ , unmute : () => formListener.start ()
73
+ , destroy : () => {
74
+ formListener.stop ()
75
+ pluginState = null
76
+ pluginAPI = null
77
+ }
78
+ }; // pluginAPI
79
+ Object.freeze ( pluginAPI )
80
+ return pluginAPI
81
+ } // pluginForm func.
82
+
83
+
84
+ export default pluginForm
@@ -61,7 +61,7 @@ function _listenDOM ( dependencies, state ) {
61
61
 
62
62
  if ( sequence ) {
63
63
  const signal = `KEY:${res.join(',')}`
64
- ev.emit ( signal, data )
64
+ ev.emit ( signal, data )
65
65
  // Reset:
66
66
  r = []
67
67
  keyTimer = null
@@ -0,0 +1,299 @@
1
+ import { beforeEach, describe, it, test } from 'vitest'
2
+ import { userEvent } from '@vitest/browser/context'
3
+
4
+ import Block from '../test-components/Block.jsx'
5
+ import VisaulController from '@peter.naydenov/visual-controller-for-react'
6
+ import '../test-components/style.css'
7
+
8
+ import {
9
+ pluginClick,
10
+ pluginKey
11
+ , pluginForm
12
+ , shortcuts
13
+ } from '../src/main.js'
14
+ import { expect } from 'chai'
15
+
16
+ import askForPromise from 'ask-for-promise'
17
+
18
+ const html = new VisaulController ();
19
+
20
+ let
21
+ a = false
22
+ , b = false
23
+ ;
24
+
25
+
26
+ function wait (ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+
30
+ const short = shortcuts ({onShortcut : ( shortcut, {context,note,type}) => console.log (shortcut, context, note, type) });
31
+
32
+ short.load ({
33
+ general : {
34
+ ' key : shift+a': [ () => a = true ]
35
+ }
36
+ , extra : {
37
+ 'key : p,r,o,b,a': () => b = true
38
+ }
39
+ , extend : {
40
+ 'form : watch' : () => 'input'
41
+ , 'form : define' : () => 'input'
42
+ , 'form : action' : () => [
43
+ {
44
+ fn : (e) => console.log ( e.target )
45
+ , type : 'input'
46
+ , mode : 'in'
47
+ }
48
+ ]
49
+ }
50
+ })
51
+
52
+ beforeEach ( () => {
53
+ let container = document.createElement ( 'div' )
54
+ container.id = 'app'
55
+ document.body.appendChild ( container )
56
+ html.publish ( Block, {}, 'app' )
57
+ a = false, b = false
58
+ }) // beforeEach
59
+
60
+
61
+
62
+
63
+
64
+ describe ( "Shortcuts", () => {
65
+
66
+
67
+
68
+ test ( 'Shortcut if no plugin installed', () => {
69
+ let res = new Promise ( (resolve,reject) => {
70
+ short.changeContext ( 'general' )
71
+ let r = short.listShortcuts ('general')
72
+ expect ( r[0]).to.equal ( ' key : shift+a' ) // Shortcut name is the same as it was set
73
+ resolve ('success')
74
+ })
75
+ return res
76
+ }) // test no plugin installed
77
+
78
+
79
+
80
+ test ( 'Key plugin, no context selected', () => {
81
+ const res = new Promise ( ( resolve ) => {
82
+ short.enablePlugin ( pluginKey )
83
+ const r = short.listShortcuts ( 'general' )
84
+ expect ( r[0] ).to.equal ( 'KEY:A+SHIFT' ) // Shortcut name is recognized by plugin and is normalized
85
+ resolve ('success')
86
+ })
87
+ return res
88
+ }) // test key plugin installed, no context selected
89
+
90
+
91
+
92
+ test ( 'Key plugin with context selected', () => {
93
+ const res = new Promise ( (resolve) => {
94
+ short.enablePlugin ( pluginKey )
95
+ short.changeContext ( 'general' )
96
+ const r = short.listShortcuts ('general')
97
+ expect ( r[0] ).to.equal ( 'KEY:A+SHIFT' ) // Shortcut name is recognized by plugin and is normalized
98
+ resolve ( 'success' )
99
+ })
100
+ return res
101
+ }) // test key plugin installed with context selected
102
+
103
+
104
+
105
+ test ( 'Simple shortcut', () => {
106
+ const res = new Promise ( (resolve) => {
107
+ short.enablePlugin ( pluginKey )
108
+ short.changeContext ()
109
+ short.changeContext ( 'general' )
110
+ userEvent.keyboard ( '{Shift>}a</Shift>' )
111
+ expect ( a ).to.be.false
112
+ wait ( 1000 )
113
+ .then ( () => { // Default wait sequence timeout is 480 ms, but maxSequence is 1, so we don't need to wait for timeout
114
+ expect ( a ).to.be.true
115
+ resolve ( 'success' )
116
+ })
117
+ })
118
+ return res
119
+ }) // test simple shortcut
120
+
121
+
122
+
123
+ test ( 'Call sequence shortcut', async () => {
124
+ let res = new Promise ( async (resolve) => {
125
+ b = false
126
+ short.enablePlugin ( pluginKey )
127
+ short.changeContext ()
128
+ short.changeContext ( 'extra' )
129
+ await userEvent.keyboard ( 'proba' )
130
+ expect ( b ).to.be.true
131
+ resolve ( 'success' )
132
+ })
133
+ return res
134
+ }) // test call sequence shortcut
135
+
136
+
137
+
138
+ test ( 'Single mouse click', done => {
139
+ const res = new Promise ( async (resolve) => {
140
+ expect ( a ).to.be.false
141
+ expect ( b ).to.be.false
142
+ short.enablePlugin ( pluginClick )
143
+
144
+ short.load ({ 'extra' : {
145
+ ' cLIck : left - 1 ' : () => a = true // Check if spaces, letter case can break the shortcut recognition
146
+ }
147
+ })
148
+ short.changeContext ()
149
+ short.changeContext ( 'extra' )
150
+ wait ( 100 )
151
+ .then ( async () => {
152
+ // Default wait mouse timeout is 320 ms, but maxClicks is 1, so we don't need to wait for timeout
153
+ let loc = document.querySelector('#rspan') || false
154
+ if ( loc ) await userEvent.click ( loc )
155
+ expect ( a ).to.be.true
156
+ resolve ( 'success' )
157
+ })
158
+
159
+
160
+ })
161
+ return res
162
+ }) // test single mouse click
163
+
164
+
165
+
166
+ test ( 'Double mouse click', () => {
167
+ let res = new Promise ( async (resolve) => {
168
+ expect ( a ).to.be.false
169
+ expect ( b ).to.be.false
170
+
171
+ short.enablePlugin ( pluginClick )
172
+ short.changeContext ( 'extra' )
173
+ short.load ({ // load will restart the selected context
174
+ 'extra' : { // load will overwrite existing 'extra' context definition
175
+ 'click: left-2' : () => a = true
176
+ }
177
+ })
178
+ wait ( 350 )
179
+ .then ( async () => { // Default wait mouse timeout is 320 ms
180
+ let loc = document.querySelector ( '#rspan' ) || false
181
+ // Third click is ignored. Max clicks according definition is 2.
182
+ if ( loc ) await userEvent.tripleClick ( loc )
183
+ expect ( a ).to.be.true
184
+ resolve ( 'success' )
185
+ })
186
+ })
187
+ return res
188
+ }) // test double mouse click
189
+
190
+
191
+
192
+ test ( 'Dependencies on shortcuts', () => {
193
+ const res = new Promise ( async (resolve) => {
194
+ const task = askForPromise ();
195
+ expect ( a ).to.be.false
196
+ expect ( b ).to.be.false
197
+
198
+ short.enablePlugin ( pluginClick )
199
+ short.setDependencies ({ task })
200
+
201
+ short.load ({
202
+ 'extra' : { // load will overwrite existing 'extra' context definition
203
+ 'click: left-1' : ({dependencies}) => {
204
+ const { task } = dependencies;
205
+ expect ( task ).to.have.property ( 'done' )
206
+ expect ( task ).to.have.property ( 'promise' )
207
+ a = true
208
+ }
209
+ }
210
+ }) // load will restart the selected context
211
+ short.changeContext ( 'extra' )
212
+ wait ( 350 ) // Default wait mouse timeout is 320 ms
213
+ .then ( async () => {
214
+ let loc = document.querySelector ( '#rspan' ) || false
215
+ if ( loc ) await userEvent.click ( loc )
216
+ resolve ( 'success' )
217
+ })
218
+ }) // res promise
219
+ return res
220
+ }) // test dependencies on shortcuts
221
+
222
+
223
+
224
+ test ( 'Emit custom event', () => {
225
+ const res = new Promise ( async (resolve) => {
226
+ let result = null;
227
+ short.changeContext ()
228
+ short.enablePlugin ( pluginClick )
229
+ const myAllContext = {
230
+ myAll: {
231
+ 'click : leff-1' : () => console.log ( 'nothing' )
232
+ , 'yo' : ({msg}) => result = msg
233
+ }}
234
+ short.load ( myAllContext )
235
+ short.changeContext ( 'myAll' )
236
+ short.emit ( 'yo', { context: short.getContext(), note: 'tt', type:'custom', msg:'hello' })
237
+ expect ( result ).to.be.equal ( 'hello' )
238
+ short.changeContext ( 'general' )
239
+ short.unload ( 'myAll' )
240
+ resolve ( 'success' )
241
+ })
242
+ return res
243
+ }) // test emit custom event
244
+
245
+
246
+
247
+ test ( 'List shortcuts', () => {
248
+ short.enablePlugin ( pluginKey )
249
+ let general = short.listShortcuts ('general');
250
+ expect ( general ).to.be.an ( 'array' )
251
+ expect ( general ).to.have.lengthOf ( 1 )
252
+ expect ( general[0] ).to.be.equal ( 'KEY:A+SHIFT' )
253
+
254
+ let fail = short.listShortcuts ( 'somethingNotExisting' );
255
+ expect ( fail ).to.be.null
256
+
257
+ let all = short.listShortcuts ();
258
+ expect ( all ).to.be.an ( 'array' )
259
+
260
+ expect ( all ).to.have.lengthOf ( 3 )
261
+ expect ( all[0] ).to.have.property ( 'context' )
262
+ expect ( all[0] ).to.have.property ( 'shortcuts' )
263
+ expect ( all[0].shortcuts ).to.be.an('array')
264
+ expect ( all[0].shortcuts ).to.have.lengthOf ( 1 )
265
+ expect ( all[0].shortcuts[0] ).to.be.equal ( 'KEY:A+SHIFT' )
266
+ expect ( all[0].context ).to.be.equal ( 'general' )
267
+ }) // test list shortcuts
268
+
269
+
270
+
271
+ test ( 'Click on anchor', () => {
272
+ const res = new Promise ( async (resolve) => {
273
+ // Click on anchor that don't have click-data attribute.
274
+ let result = 'none';
275
+ short.enablePlugin ( pluginClick )
276
+ short.load ({ 'extra' : {
277
+ 'click: 1 - left' : ({target, context, event }) => { // Order of button name and number of click is not important
278
+ event.preventDefault ()
279
+ expect ( context ).to.be.equal ( 'extra' )
280
+ expect ( target.nodeName ).to.be.equal ( 'A' )
281
+ result = target.nodeName
282
+ }
283
+ }
284
+ })
285
+ short.changeContext ( 'extra' )
286
+ wait ( 10 )
287
+ .then ( async () => {
288
+ let loc = document.querySelector ( '#anchor' ) || false;
289
+ if ( loc ) await userEvent.click ( loc )
290
+ expect ( result ).to.be.equal ( 'A' )
291
+ short.changeContext ( 'general' )
292
+ resolve ( 'success' )
293
+ })
294
+ })
295
+ return res
296
+ }) // test click on anchor
297
+
298
+ }) // describe
299
+
@@ -3,6 +3,8 @@ return <>
3
3
  <div className="block" data-click="red"><span id='rspan'>Red</span> </div>
4
4
  <button className="big-btn" data-click="mega">Mega button</button>
5
5
  <p>Some text with <a href="#" target="_blank">link <span id="anchor">in</span></a> it</p>
6
+ <p><input id="name" type="text" /></p>
7
+ <p><input type="text" id="age" /></p>
6
8
  </>
7
9
  }
8
10
 
@@ -0,0 +1,9 @@
1
+ export default function HelloWorld({ name }) {
2
+ const parent = document.createElement('div')
3
+
4
+ const h1 = document.createElement('h1')
5
+ h1.textContent = 'Hello ' + name + '!'
6
+ parent.appendChild(h1)
7
+
8
+ return parent
9
+ }
@@ -0,0 +1,11 @@
1
+ import { expect, test } from 'vitest'
2
+ import { getByText } from '@testing-library/dom'
3
+ import HelloWorld from './HelloWorld.js'
4
+
5
+ test('renders name', () => {
6
+ const parent = HelloWorld({ name: 'Vitest' })
7
+ document.body.appendChild(parent)
8
+
9
+ const element = getByText(parent, 'Hello Vitest!')
10
+ expect(element).toBeInTheDocument()
11
+ })
@@ -0,0 +1,19 @@
1
+ import { defineWorkspace } from 'vitest/config'
2
+
3
+ export default defineWorkspace([
4
+ // If you want to keep running your existing tests in Node.js, uncomment the next line.
5
+ // 'vite.config.js',
6
+ {
7
+ extends: 'vite.config.js',
8
+ test: {
9
+ environment: 'browser',
10
+ browser: {
11
+ enabled: true,
12
+ name: 'chromium',
13
+ provider: 'playwright',
14
+ // https://vitest.dev/guide/browser/playwright
15
+ configs: [],
16
+ },
17
+ },
18
+ },
19
+ ])
@@ -1,5 +0,0 @@
1
- {
2
- "name": "Using fixtures to represent data",
3
- "email": "hello@cypress.io",
4
- "body": "Fixtures are a great way to mock data for responses to routes"
5
- }
@@ -1,25 +0,0 @@
1
- // ***********************************************
2
- // This example commands.js shows you how to
3
- // create various custom commands and overwrite
4
- // existing commands.
5
- //
6
- // For more comprehensive examples of custom
7
- // commands please read more here:
8
- // https://on.cypress.io/custom-commands
9
- // ***********************************************
10
- //
11
- //
12
- // -- This is a parent command --
13
- // Cypress.Commands.add('login', (email, password) => { ... })
14
- //
15
- //
16
- // -- This is a child command --
17
- // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18
- //
19
- //
20
- // -- This is a dual command --
21
- // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22
- //
23
- //
24
- // -- This will overwrite an existing command --
25
- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
@@ -1,14 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
7
- <title>Components App</title>
8
- <!-- Used by Next.js to inject CSS. -->
9
- <div id="__next_css__DO_NOT_USE__"></div>
10
- </head>
11
- <body>
12
- <div data-cy-root></div>
13
- </body>
14
- </html>
@@ -1,27 +0,0 @@
1
- // ***********************************************************
2
- // This example support/component.js is processed and
3
- // loaded automatically before your test files.
4
- //
5
- // This is a great place to put global configuration and
6
- // behavior that modifies Cypress.
7
- //
8
- // You can change the location of this file or turn off
9
- // automatically serving support files with the
10
- // 'supportFile' configuration option.
11
- //
12
- // You can read more here:
13
- // https://on.cypress.io/configuration
14
- // ***********************************************************
15
-
16
- // Import commands.js using ES2015 syntax:
17
- import './commands'
18
-
19
- // Alternatively you can use CommonJS syntax:
20
- // require('./commands')
21
-
22
- import { mount } from 'cypress/react18'
23
-
24
- Cypress.Commands.add('mount', mount)
25
-
26
- // Example use:
27
- // cy.mount(<MyComponent />)
@@ -1,20 +0,0 @@
1
- // ***********************************************************
2
- // This example support/e2e.js is processed and
3
- // loaded automatically before your test files.
4
- //
5
- // This is a great place to put global configuration and
6
- // behavior that modifies Cypress.
7
- //
8
- // You can change the location of this file or turn off
9
- // automatically serving support files with the
10
- // 'supportFile' configuration option.
11
- //
12
- // You can read more here:
13
- // https://on.cypress.io/configuration
14
- // ***********************************************************
15
-
16
- // Import commands.js using ES2015 syntax:
17
- import './commands'
18
-
19
- // Alternatively you can use CommonJS syntax:
20
- // require('./commands')
package/cypress.config.js DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "cypress";
2
-
3
- export default defineConfig({
4
- component: {
5
- devServer: {
6
- framework: "react",
7
- bundler: "vite",
8
- },
9
- },
10
- });