@defra/flood-webchat 0.0.1-alpha.9 → 0.0.1-beta.2
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/README.md +1 -83
- package/dist/client.js +445 -0
- package/dist/client.js.map +1 -0
- package/dist/server.js +362 -0
- package/dist/server.js.map +1 -0
- package/docs/development-guide.md +1 -2
- package/jest.config.mjs +29 -0
- package/main.scss +5 -741
- package/package.json +18 -13
- package/src/client/components/availability/availability.jsx +101 -0
- package/src/client/components/availability/availability.scss +89 -0
- package/src/client/index.jsx +16 -0
- package/src/client/lib/check-availability.js +7 -0
- package/src/client/lib/classnames.js +1 -0
- package/src/client/lib/external-stores.js +4 -0
- package/src/client/lib/external-sync-store.js +42 -0
- package/src/server/index.js +11 -6
- package/src/server/{client.js → lib/client.js} +32 -12
- package/src/server/{utils.js → lib/utils.js} +8 -13
- package/webpack.config.mjs +32 -0
- package/assets/audio/notification.mp3 +0 -0
- package/babel.config.cjs +0 -3
- package/dist/templates.js +0 -554
- package/src/client/index.js +0 -14
- package/src/client/lib/availability.js +0 -75
- package/src/client/lib/config.js +0 -17
- package/src/client/lib/keyboard.js +0 -149
- package/src/client/lib/notification.js +0 -59
- package/src/client/lib/nunjucks.js +0 -3
- package/src/client/lib/panel.js +0 -293
- package/src/client/lib/provider.js +0 -11
- package/src/client/lib/skiplink.js +0 -27
- package/src/client/lib/state.js +0 -82
- package/src/client/lib/transcript.js +0 -34
- package/src/client/lib/utils.js +0 -190
- package/src/client/lib/webchat.js +0 -1075
- package/src/style/_mixins.scss +0 -13
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import Utils from './utils.js'
|
|
2
|
-
|
|
3
|
-
class Keyboard {
|
|
4
|
-
static _isKeyboard = false
|
|
5
|
-
|
|
6
|
-
static init (state) {
|
|
7
|
-
this._state = state
|
|
8
|
-
this._detectFocus()
|
|
9
|
-
|
|
10
|
-
Utils.listenForDevice('mobile', this._updateInert.bind(this))
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static _detectFocus () {
|
|
14
|
-
document.addEventListener('keydown', e => {
|
|
15
|
-
this._isKeyboard = true
|
|
16
|
-
if (e.key === 'Tab' || e.key === 'Escape' || e.key === 'Esc') {
|
|
17
|
-
this._constrainFocus(e)
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
document.addEventListener('keyup', e => {
|
|
22
|
-
this._isKeyboard = true
|
|
23
|
-
if (e.key === 'Tab') {
|
|
24
|
-
const el = this._focusParent()
|
|
25
|
-
if (el) {
|
|
26
|
-
this._toggleInert(el)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
document.addEventListener('pointerdown', e => {
|
|
32
|
-
this._isKeyboard = false
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
document.addEventListener('focus', e => {
|
|
36
|
-
const isScope = !!e.target.closest('#wc-availability, #wc-panel')
|
|
37
|
-
if (!isScope) {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
const target = e.target.hasAttribute('data-wc-focus-parent') ? e.target.parentNode : e.target
|
|
41
|
-
target.classList.toggle('wc-focus-visible', this._isKeyboard)
|
|
42
|
-
}, true)
|
|
43
|
-
|
|
44
|
-
document.addEventListener('blur', e => {
|
|
45
|
-
const target = e.target.hasAttribute('data-wc-focus-parent') ? e.target.parentNode : e.target
|
|
46
|
-
target.classList.remove('wc-focus-visible')
|
|
47
|
-
}, true)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static _isFocusable (el) {
|
|
51
|
-
const tagNames = ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT']
|
|
52
|
-
const isHidden = el.hidden || el.style.display === 'none'
|
|
53
|
-
const isTextbox = el.getAttribute('role') === 'textbox'
|
|
54
|
-
|
|
55
|
-
return (tagNames.includes(el.tagName) || el.tabIndex >= 0 || isTextbox) && !isHidden
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static _focusParent () {
|
|
59
|
-
const el = document.querySelector('[data-wc]')
|
|
60
|
-
|
|
61
|
-
if (el && !document.activeElement.closest('[data-wc]')) {
|
|
62
|
-
el.focus()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return el
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static _constrainFocus (e) {
|
|
69
|
-
const el = this._focusParent()
|
|
70
|
-
if (!el) {
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const focusableEls = this._getFocusableEls(el)
|
|
75
|
-
const firstFocusableEl = focusableEls[0]
|
|
76
|
-
const lastFocusableEl = focusableEls[focusableEls.length - 1]
|
|
77
|
-
|
|
78
|
-
if (e.shiftKey) {
|
|
79
|
-
if (document.activeElement === firstFocusableEl) {
|
|
80
|
-
lastFocusableEl.focus()
|
|
81
|
-
e.preventDefault()
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
if (document.activeElement === lastFocusableEl) {
|
|
85
|
-
firstFocusableEl.focus()
|
|
86
|
-
e.preventDefault()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
static _toggleInert (el) {
|
|
92
|
-
const inert = document.querySelectorAll('[data-wc-inert]')
|
|
93
|
-
for (let i = 0; i < inert.length; i++) {
|
|
94
|
-
const el = inert[i]
|
|
95
|
-
el.removeAttribute('aria-hidden')
|
|
96
|
-
el.removeAttribute('data-wc-inert')
|
|
97
|
-
}
|
|
98
|
-
if (el) {
|
|
99
|
-
while (el.parentNode && el !== document.body) {
|
|
100
|
-
let sibling = el.parentNode.firstChild
|
|
101
|
-
while (sibling) {
|
|
102
|
-
if (sibling.nodeType === 1 && sibling !== el) {
|
|
103
|
-
if (!sibling.hasAttribute('aria-hidden')) {
|
|
104
|
-
sibling.setAttribute('aria-hidden', true)
|
|
105
|
-
sibling.setAttribute('data-wc-inert', '')
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
sibling = sibling.nextSibling
|
|
109
|
-
}
|
|
110
|
-
el = el.parentNode
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
static _updateInert () {
|
|
116
|
-
const isOpen = this._state.isOpen
|
|
117
|
-
const activeElement = document.activeElement
|
|
118
|
-
const isWithinDialog = activeElement && !!document.activeElement.closest('[data-wc][role="dialog"][open]')
|
|
119
|
-
const containerEl = document.querySelector('[data-wc]')
|
|
120
|
-
if (!isWithinDialog) {
|
|
121
|
-
this._toggleInert(isOpen ? containerEl : null)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
static _getFocusableEls (el) {
|
|
126
|
-
const selectors = [
|
|
127
|
-
'a[href]:not([disabled])',
|
|
128
|
-
'button:not([disabled])',
|
|
129
|
-
'textarea:not([disabled])',
|
|
130
|
-
'input:not([disabled])',
|
|
131
|
-
'select:not([disabled])',
|
|
132
|
-
'*[tabindex="0"]:not([disabled])'
|
|
133
|
-
]
|
|
134
|
-
let focusableEls = Array.from(el.querySelectorAll(selectors.join(',')))
|
|
135
|
-
focusableEls = focusableEls.filter(e => !e.closest('[hidden]') && !e.closest('[aria-hidden="true"]'))
|
|
136
|
-
return focusableEls
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
static toggleInert (el) {
|
|
140
|
-
this._toggleInert(el)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
static getFirstFocusableEl () {
|
|
144
|
-
const focusableEls = this._getFocusableEls(document)
|
|
145
|
-
return focusableEls.length ? focusableEls[0] : null
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default Keyboard
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
export default class Notification {
|
|
2
|
-
constructor (url) {
|
|
3
|
-
this._url = url
|
|
4
|
-
|
|
5
|
-
// Create context
|
|
6
|
-
const context = new (window.AudioContext || window.webkitAudioContext)()
|
|
7
|
-
// context.addEventListener('statechange', () => {
|
|
8
|
-
// console.log('Audio: ', context.state)
|
|
9
|
-
// })
|
|
10
|
-
this._context = context
|
|
11
|
-
|
|
12
|
-
// Unlock audio
|
|
13
|
-
this._unlockAudioContext()
|
|
14
|
-
|
|
15
|
-
// Load buffer
|
|
16
|
-
this._init(url)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_init () {
|
|
20
|
-
const context = this._context
|
|
21
|
-
const url = this._url
|
|
22
|
-
|
|
23
|
-
fetch(url)
|
|
24
|
-
.then(response => response.arrayBuffer())
|
|
25
|
-
.then(buffer => context.decodeAudioData(buffer))
|
|
26
|
-
.then(decodedData => {
|
|
27
|
-
this._buffer = decodedData
|
|
28
|
-
})
|
|
29
|
-
.catch(console.error)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
_unlockAudioContext () {
|
|
33
|
-
const context = this._context
|
|
34
|
-
if (context.state === 'suspended') {
|
|
35
|
-
const events = ['touchstart', 'touchend', 'mousedown', 'wheel', 'keydown', 'click']
|
|
36
|
-
const unlock = e => {
|
|
37
|
-
events.forEach(event => {
|
|
38
|
-
document.body.removeEventListener(event, unlock)
|
|
39
|
-
})
|
|
40
|
-
context.resume()
|
|
41
|
-
}
|
|
42
|
-
events.forEach(event => {
|
|
43
|
-
document.body.addEventListener(event, unlock, false)
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
playSound () {
|
|
49
|
-
const context = this._context
|
|
50
|
-
const buffer = this._buffer
|
|
51
|
-
console.log('Play sound (context state: ', context.state, ')')
|
|
52
|
-
|
|
53
|
-
// Create source
|
|
54
|
-
const source = context.createBufferSource()
|
|
55
|
-
source.buffer = buffer
|
|
56
|
-
source.connect(context.destination)
|
|
57
|
-
source.start()
|
|
58
|
-
}
|
|
59
|
-
}
|
package/src/client/lib/panel.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { Button, CharacterCount } from 'govuk-frontend'
|
|
2
|
-
import Utils from './utils.js'
|
|
3
|
-
import Config from './config.js'
|
|
4
|
-
import Keyboard from './keyboard.js'
|
|
5
|
-
import nunjucks from './nunjucks.js'
|
|
6
|
-
|
|
7
|
-
class Panel {
|
|
8
|
-
constructor () {
|
|
9
|
-
this._container = null
|
|
10
|
-
this._timeout = Utils.getDuration(Config.timeout)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
_initComponents (el) {
|
|
14
|
-
// Initialise GOV.UK components
|
|
15
|
-
const buttons = el.querySelectorAll('[data-module="govuk-button"]')
|
|
16
|
-
for (const button of buttons) {
|
|
17
|
-
new Button(button).init()
|
|
18
|
-
}
|
|
19
|
-
const characterCounts = el.querySelectorAll('[data-module="govuk-character-count"]')
|
|
20
|
-
for (const characterCount of characterCounts) {
|
|
21
|
-
new CharacterCount(characterCount).init()
|
|
22
|
-
}
|
|
23
|
-
// Initialise autoresize
|
|
24
|
-
const textbox = el.querySelector('[data-wc-textbox]')
|
|
25
|
-
if (textbox) {
|
|
26
|
-
Utils.autosize(textbox)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_updateMessagesLabel (total) {
|
|
31
|
-
const body = this.body
|
|
32
|
-
const label = `Conversation with ${total} message${total > 1 ? 's' : ''}`
|
|
33
|
-
body.setAttribute('aria-label', label)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
_scrollToLatest (isScroll) {
|
|
37
|
-
if (!isScroll) {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Scroll to latest
|
|
42
|
-
this.body.scrollTop = this.body.scrollHeight
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
create (state, addEvents) {
|
|
46
|
-
console.log('panel.create()')
|
|
47
|
-
|
|
48
|
-
const model = {
|
|
49
|
-
...state
|
|
50
|
-
}
|
|
51
|
-
const html = nunjucks.render('panel.html', { model })
|
|
52
|
-
document.body.insertAdjacentHTML('beforeend', html)
|
|
53
|
-
|
|
54
|
-
const container = document.querySelector('[data-wc]')
|
|
55
|
-
const header = container.querySelector('[data-wc-header]')
|
|
56
|
-
const body = container.querySelector('[data-wc-body]')
|
|
57
|
-
const footer = container.querySelector('[data-wc-footer]')
|
|
58
|
-
this.container = container
|
|
59
|
-
this.header = header
|
|
60
|
-
this.body = body
|
|
61
|
-
this.footer = footer
|
|
62
|
-
|
|
63
|
-
// Move focus to container and set inert
|
|
64
|
-
container.focus()
|
|
65
|
-
Keyboard.toggleInert(container)
|
|
66
|
-
|
|
67
|
-
Utils.listenForDevice('mobile', this.setAttributes.bind(this, state))
|
|
68
|
-
|
|
69
|
-
// Message events
|
|
70
|
-
container.addEventListener('keyup', e => {
|
|
71
|
-
if (e.target.hasAttribute('data-wc-textbox')) {
|
|
72
|
-
const textbox = e.target
|
|
73
|
-
const label = textbox.previousElementSibling
|
|
74
|
-
// Conditionally show label
|
|
75
|
-
Utils.toggleLabel(label, e.key, textbox)
|
|
76
|
-
// Conditionally submit form
|
|
77
|
-
Utils.submit(e, textbox)
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
container.addEventListener('keydown', e => {
|
|
81
|
-
if (e.target.hasAttribute('data-wc-textbox')) {
|
|
82
|
-
const textbox = e.target
|
|
83
|
-
const label = textbox.previousElementSibling
|
|
84
|
-
// Conditionally hide label
|
|
85
|
-
Utils.toggleLabel(label, e.key, textbox)
|
|
86
|
-
// Conditionally suppress enter
|
|
87
|
-
Utils.suppressEnter(e, textbox)
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
container.addEventListener('change', e => {
|
|
91
|
-
if (e.target.hasAttribute('data-wc-textbox')) {
|
|
92
|
-
console.log('change', e.target)
|
|
93
|
-
const textbox = e.target
|
|
94
|
-
const label = textbox.previousElementSibling
|
|
95
|
-
Utils.toggleLabel(label, null, textbox)
|
|
96
|
-
}
|
|
97
|
-
}, true)
|
|
98
|
-
|
|
99
|
-
// Add panel events
|
|
100
|
-
addEvents()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
update (state, error) {
|
|
104
|
-
console.log('panel.updateAll()')
|
|
105
|
-
|
|
106
|
-
const container = document.getElementById('wc-panel')
|
|
107
|
-
if (!container) {
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Update labelledyby
|
|
112
|
-
this.container.setAttribute('aria-labelledyby', error ? 'wc-title wc-subtitle wc-error' : 'wc-title wc-subtitle')
|
|
113
|
-
|
|
114
|
-
this.updateHeader(state)
|
|
115
|
-
this.updateBody(state, error)
|
|
116
|
-
this.updateFooter(state)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
updateHeader (state) {
|
|
120
|
-
console.log('panel.updateHeader()')
|
|
121
|
-
|
|
122
|
-
const container = document.getElementById('wc-panel')
|
|
123
|
-
if (!container) {
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Model
|
|
128
|
-
const model = {
|
|
129
|
-
...state
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const header = this.header
|
|
133
|
-
header.innerHTML = nunjucks.render('header.html', { model })
|
|
134
|
-
|
|
135
|
-
this._initComponents(header)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
updateBody (state, error) {
|
|
139
|
-
console.log('panel.updateBody()')
|
|
140
|
-
|
|
141
|
-
const container = document.getElementById('wc-panel')
|
|
142
|
-
if (!container) {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Model
|
|
147
|
-
const model = {
|
|
148
|
-
...state,
|
|
149
|
-
error,
|
|
150
|
-
timeout: this._timeout
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const body = this.body
|
|
154
|
-
body.innerHTML = nunjucks.render('body.html', { model })
|
|
155
|
-
|
|
156
|
-
// Toggle body keyboard accessibility
|
|
157
|
-
const isViewOpen = state.view === 'OPEN'
|
|
158
|
-
if (isViewOpen) {
|
|
159
|
-
body.tabIndex = 0
|
|
160
|
-
const total = state.messages.length
|
|
161
|
-
this._updateMessagesLabel(total)
|
|
162
|
-
this._scrollToLatest(state.isScroll)
|
|
163
|
-
} else {
|
|
164
|
-
body.removeAttribute('tabindex')
|
|
165
|
-
body.removeAttribute('aria-label')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
this._initComponents(body)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
updateFooter (state) {
|
|
172
|
-
console.log('panel.updateFooter()')
|
|
173
|
-
|
|
174
|
-
const container = document.getElementById('wc-panel')
|
|
175
|
-
if (!container) {
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Model
|
|
180
|
-
const model = {
|
|
181
|
-
...state
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const footer = this.footer
|
|
185
|
-
footer.innerHTML = nunjucks.render('footer.html', { model })
|
|
186
|
-
|
|
187
|
-
this._initComponents(footer)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
addMessage (state, message) {
|
|
191
|
-
const list = this.container.querySelector('[data-wc-list]')
|
|
192
|
-
if (!list) {
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
list.insertAdjacentHTML('beforeend', message.html)
|
|
197
|
-
|
|
198
|
-
// Update message label
|
|
199
|
-
const total = state.messages.length
|
|
200
|
-
this._updateMessagesLabel(total)
|
|
201
|
-
|
|
202
|
-
// Scroll messages
|
|
203
|
-
this._scrollToLatest(state.isScroll)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
toggleAgentTyping (state, isTyping) {
|
|
207
|
-
const list = this.body.querySelector('[data-wc-list]')
|
|
208
|
-
if (!list) {
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Add or remove elements from list
|
|
213
|
-
const el = list.querySelector('[data-wc-agent-typing]')
|
|
214
|
-
if (isTyping) {
|
|
215
|
-
list.insertAdjacentHTML('beforeend', nunjucks.render('agent-typing.html', {
|
|
216
|
-
model: {
|
|
217
|
-
name: state.assignee
|
|
218
|
-
}
|
|
219
|
-
}))
|
|
220
|
-
|
|
221
|
-
// Scroll to show new elements
|
|
222
|
-
this._scrollToLatest(state.isScroll)
|
|
223
|
-
} else if (el) {
|
|
224
|
-
el.remove()
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
toggleTimeout (state, hasTimeout) {
|
|
229
|
-
const container = this.container
|
|
230
|
-
const timeout = container.querySelector('[data-wc-timeout]')
|
|
231
|
-
|
|
232
|
-
if (hasTimeout) {
|
|
233
|
-
const list = container.querySelector('[data-wc-list]')
|
|
234
|
-
|
|
235
|
-
// *** If we have the list but don't already have the timeout markup
|
|
236
|
-
if (list && !timeout) {
|
|
237
|
-
list.insertAdjacentHTML('afterend', `
|
|
238
|
-
<div class="wc-timeout" data-wc-timeout>
|
|
239
|
-
<div class="wc-timeout__inner">
|
|
240
|
-
<div class="wc-timeout__message">Webchat will end in <span data-wc-countdown>${this._timeout} seconds</span></div>
|
|
241
|
-
</div>
|
|
242
|
-
<a href="#" class="wc-cancel-timeout-btn" data-wc-cancel-timeout>Continue webchat</a>
|
|
243
|
-
</div>
|
|
244
|
-
`)
|
|
245
|
-
this._scrollToLatest(state.isScroll)
|
|
246
|
-
|
|
247
|
-
// Clear timeout
|
|
248
|
-
const clearBtn = container.querySelector('[data-wc-cancel-timeout]')
|
|
249
|
-
clearBtn.addEventListener('click', e => {
|
|
250
|
-
e.preventDefault()
|
|
251
|
-
this._resetTimeout()
|
|
252
|
-
})
|
|
253
|
-
}
|
|
254
|
-
} else if (timeout) {
|
|
255
|
-
timeout.remove()
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
setAttributes (state) {
|
|
260
|
-
const container = this.container
|
|
261
|
-
if (!container) {
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const isFullscreen = state.isMobile && state.isOpen
|
|
266
|
-
const root = document.getElementsByTagName('html')[0]
|
|
267
|
-
root.classList.toggle('wc-html', isFullscreen)
|
|
268
|
-
document.body.classList.toggle('wc-body', isFullscreen)
|
|
269
|
-
container.setAttribute('aria-modal', true)
|
|
270
|
-
|
|
271
|
-
const backBtn = container.querySelector('[data-wc-back-btn]')
|
|
272
|
-
const hideBtn = container.querySelector('[data-wc-hide-btn]')
|
|
273
|
-
const closeBtn = container.querySelector('[data-wc-close-btn]')
|
|
274
|
-
|
|
275
|
-
if (backBtn) {
|
|
276
|
-
backBtn.hidden = !isFullscreen
|
|
277
|
-
}
|
|
278
|
-
if (hideBtn) {
|
|
279
|
-
hideBtn.hidden = isFullscreen
|
|
280
|
-
}
|
|
281
|
-
if (closeBtn) {
|
|
282
|
-
closeBtn.hidden = isFullscreen
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Toggle textbox behaviour
|
|
286
|
-
const textbox = container.querySelector('[data-wc-textbox]')
|
|
287
|
-
if (textbox) {
|
|
288
|
-
textbox.toggleAttribute('data-wc-enter-submit', !state.isMobile)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export default Panel
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import nunjucks from './nunjucks.js'
|
|
2
|
-
|
|
3
|
-
class Skiplink {
|
|
4
|
-
constructor () {
|
|
5
|
-
this.target = document.querySelector('a[data-module="govuk-skip-link"]')
|
|
6
|
-
this.skiplink = null
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
toggle (hasSkip) {
|
|
10
|
-
const target = this.target
|
|
11
|
-
const skiplink = this.skiplink
|
|
12
|
-
if (!target) {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (hasSkip && !skiplink) {
|
|
17
|
-
// Add skiplink
|
|
18
|
-
target.insertAdjacentHTML('afterend', nunjucks.render('skiplink.html'))
|
|
19
|
-
this.skiplink = target.nextElementSibling
|
|
20
|
-
} else if (skiplink && !hasSkip) {
|
|
21
|
-
// Remove skiplink
|
|
22
|
-
this.skiplink.remove()
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default Skiplink
|
package/src/client/lib/state.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import Utils from './utils.js'
|
|
2
|
-
|
|
3
|
-
class State {
|
|
4
|
-
constructor (openChat, closeChat) {
|
|
5
|
-
const threadId = window.localStorage.getItem('THREAD_ID')
|
|
6
|
-
const isBack = window.sessionStorage.getItem('IS_BACK') === 'true'
|
|
7
|
-
const isOpen = window.location.hash === '#webchat'
|
|
8
|
-
const view = threadId ? 'OPEN' : null
|
|
9
|
-
|
|
10
|
-
// Settings
|
|
11
|
-
const settings = window.localStorage.getItem('SETTINGS')
|
|
12
|
-
const hasAudio = settings ? settings.split(',')[0] : true
|
|
13
|
-
const isScroll = settings ? settings.split(',')[1] : true
|
|
14
|
-
|
|
15
|
-
// Public properties
|
|
16
|
-
this.threadId = threadId
|
|
17
|
-
this.availability = null
|
|
18
|
-
this.status = null
|
|
19
|
-
this.view = view
|
|
20
|
-
this.assignee = null
|
|
21
|
-
this.unseen = 0
|
|
22
|
-
this.isAuthorised = false
|
|
23
|
-
this.hasAudio = hasAudio
|
|
24
|
-
this.isScroll = isScroll
|
|
25
|
-
this.isMobile = true
|
|
26
|
-
this.isBack = isBack
|
|
27
|
-
this.isOpen = isOpen
|
|
28
|
-
this.messages = []
|
|
29
|
-
this.name = null
|
|
30
|
-
this.question = null
|
|
31
|
-
|
|
32
|
-
// Private methods (callbacks)
|
|
33
|
-
this._openChat = openChat
|
|
34
|
-
this._closeChat = closeChat
|
|
35
|
-
|
|
36
|
-
// Help with browser back behaviour
|
|
37
|
-
if (window.history.length <= 1) {
|
|
38
|
-
this._isBack = false
|
|
39
|
-
window.sessionStorage.removeItem('IS_BACK')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// We need to toggle some attributes depending on screen size
|
|
43
|
-
Utils.listenForDevice('mobile', (isMobile) => { this.isMobile = isMobile })
|
|
44
|
-
|
|
45
|
-
// Events
|
|
46
|
-
window.addEventListener('popstate', this._popstate.bind(this))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
_popstate (e) {
|
|
50
|
-
if (window.location.hash === '#webchat') {
|
|
51
|
-
this._openChat(e)
|
|
52
|
-
} else {
|
|
53
|
-
this._closeChat(e)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
replaceState () {
|
|
58
|
-
this.isOpen = false
|
|
59
|
-
const url = window.location.href.split('#')[0]
|
|
60
|
-
window.history.replaceState(null, null, url)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
pushState () {
|
|
64
|
-
this._isOpen = true
|
|
65
|
-
this._isBack = true
|
|
66
|
-
const url = `${window.location.href.split('#')[0]}#webchat`
|
|
67
|
-
window.history.pushState({ view: 'webchat', isBack: true }, '', url)
|
|
68
|
-
window.sessionStorage.setItem('IS_BACK', true)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
back () {
|
|
72
|
-
const offsetY = window.scrollY
|
|
73
|
-
window.history.back()
|
|
74
|
-
if (!this._isMobile) {
|
|
75
|
-
window.addEventListener('scroll', e => {
|
|
76
|
-
window.scrollTo(0, offsetY)
|
|
77
|
-
}, { once: true })
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export default State
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
class Transcript {
|
|
4
|
-
constructor (messages) {
|
|
5
|
-
const text = this._buildString(messages)
|
|
6
|
-
this.data = `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
_buildString (messages) {
|
|
10
|
-
const dateNow = this._formatDate(new Date())
|
|
11
|
-
let string = `Floodline webchat transcript at ${dateNow}\n\n`
|
|
12
|
-
for (let i = 0; i < messages.length; i++) {
|
|
13
|
-
const author = messages[i].direction === 'inbound' ? messages[i].user : `${messages[i].assignee} (Floodline adviser)`
|
|
14
|
-
const date = this._formatDate(messages[i].createdAt)
|
|
15
|
-
string += `${author} at ${date}\n${messages[i].text}\n\n`
|
|
16
|
-
}
|
|
17
|
-
string = string.replace(/<a\b[^>]*>/i, '').replace(/<\/a>/i, '')
|
|
18
|
-
return string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
_formatDate (datetime) {
|
|
22
|
-
let hours = datetime.getHours()
|
|
23
|
-
let minutes = datetime.getMinutes()
|
|
24
|
-
minutes = `${minutes < 10 ? '0' : ''}${minutes}`
|
|
25
|
-
const ampm = hours >= 12 ? 'pm' : 'am'
|
|
26
|
-
hours %= 12
|
|
27
|
-
hours = hours || 12
|
|
28
|
-
const time = `${hours}:${minutes}${ampm}`
|
|
29
|
-
const date = datetime.toLocaleString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })
|
|
30
|
-
return `${time}, ${date}`
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export default Transcript
|