@defra/flood-webchat 0.0.1-alpha.3 → 0.0.1-alpha.4
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 +73 -1
- package/babel.config.cjs +3 -0
- package/dist/templates.js +554 -0
- package/docs/development-guide.md +2 -1
- package/main.scss +743 -2
- package/package.json +13 -4
- package/src/client/index.js +12 -2
- package/src/client/lib/availability.js +75 -0
- package/src/client/lib/config.js +17 -0
- package/src/client/lib/keyboard.js +149 -0
- package/src/client/lib/notification.js +62 -0
- package/src/client/lib/nunjucks.js +3 -0
- package/src/client/lib/panel.js +293 -0
- package/src/client/lib/provider.js +11 -0
- package/src/client/lib/skiplink.js +27 -0
- package/src/client/lib/state.js +82 -0
- package/src/client/lib/transcript.js +34 -0
- package/src/client/lib/utils.js +190 -0
- package/src/client/lib/webchat.js +1073 -0
- package/src/server/client.js +72 -0
- package/src/server/index.js +45 -2
- package/src/server/utils.js +19 -0
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/flood-webchat",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server/index.js",
|
|
7
7
|
"browser": "src/client/index.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "npm run lint && npm run unit-test",
|
|
10
|
-
"dev": "
|
|
10
|
+
"dev": "npm run compile-templates && node -r dotenv/config node_modules/.bin/webpack server --config demo/webpack.config.mjs",
|
|
11
11
|
"unit-test": "jest",
|
|
12
12
|
"lint": "npm run lint:js && npm run lint:scss",
|
|
13
13
|
"lint:fix": "npm run lint:js -- --fix && npm run lint:scss -- --fix",
|
|
14
14
|
"lint:js": "standard",
|
|
15
15
|
"lint:scss": "stylelint \"**/*.scss\"",
|
|
16
|
-
"
|
|
16
|
+
"postinstall": "npm run compile-templates",
|
|
17
|
+
"prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') { throw e } }\"",
|
|
18
|
+
"compile-templates": "mkdir -p dist && nunjucks-precompile templates > ./dist/templates.js"
|
|
17
19
|
},
|
|
18
20
|
"standard": {
|
|
19
21
|
"ignore": [
|
|
@@ -34,21 +36,28 @@
|
|
|
34
36
|
"babel-loader": "^9.1.3",
|
|
35
37
|
"core-js": "^2.6.11",
|
|
36
38
|
"css-loader": "^6.8.1",
|
|
39
|
+
"dotenv": "^16.3.1",
|
|
37
40
|
"husky": "^8.0.3",
|
|
38
41
|
"jest": "^29.6.3",
|
|
39
42
|
"jest-environment-jsdom": "^29.6.3",
|
|
40
43
|
"mini-css-extract-plugin": "^2.7.6",
|
|
44
|
+
"node-cache": "^5.1.2",
|
|
41
45
|
"node-sass": "^9.0.0",
|
|
42
46
|
"sass": "^1.66.1",
|
|
43
47
|
"sass-loader": "^13.3.2",
|
|
44
48
|
"standard": "^17.1.0",
|
|
45
49
|
"stylelint": "^15.10.3",
|
|
46
|
-
"stylelint-config-
|
|
50
|
+
"stylelint-config-gds": "^1.0.0",
|
|
47
51
|
"webpack": "^5.88.2",
|
|
48
52
|
"webpack-cli": "^5.1.4",
|
|
49
53
|
"webpack-dev-server": "^4.15.1"
|
|
50
54
|
},
|
|
51
55
|
"peerDependencies": {
|
|
52
56
|
"govuk-frontend": "^3.14.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@nice-devone/nice-cxone-chat-web-sdk": "1.3.0",
|
|
60
|
+
"axios": "^0.24.0",
|
|
61
|
+
"nunjucks": "^3.2.4"
|
|
53
62
|
}
|
|
54
63
|
}
|
package/src/client/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import WebChat from './lib/webchat.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} id
|
|
5
|
+
* @param {object} options
|
|
6
|
+
* @param {string} options.brandId
|
|
7
|
+
* @param {string} options.channelId
|
|
8
|
+
* @param {string} options.environmentName
|
|
9
|
+
* @param {string} options.availabilityEndpoint
|
|
10
|
+
**/
|
|
11
|
+
export function init (id, options) {
|
|
12
|
+
return new WebChat(id, options)
|
|
3
13
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import nunjucks from './nunjucks.js'
|
|
2
|
+
|
|
3
|
+
class Availability {
|
|
4
|
+
constructor (id, openChatCb) {
|
|
5
|
+
// Set availability container
|
|
6
|
+
this.container = document.getElementById(id)
|
|
7
|
+
|
|
8
|
+
// Events
|
|
9
|
+
document.addEventListener('click', e => {
|
|
10
|
+
if (e.target.hasAttribute('data-wc-open-btn')) {
|
|
11
|
+
console.log('click', e.target.id)
|
|
12
|
+
openChatCb(e, e.target.id)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
document.addEventListener('keydown', e => {
|
|
16
|
+
if (e.key === ' ' && e.target.hasAttribute('data-wc-open-btn')) {
|
|
17
|
+
e.preventDefault()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
document.addEventListener('keyup', e => {
|
|
21
|
+
if (e.key === ' ' && e.target.hasAttribute('data-wc-open-btn')) {
|
|
22
|
+
openChatCb(e, e.target.id)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update (state) {
|
|
28
|
+
console.log('availability.update()')
|
|
29
|
+
|
|
30
|
+
const container = this.container
|
|
31
|
+
const isText = !container.hasAttribute('data-wc-no-text')
|
|
32
|
+
const hasThread = state.hasThread
|
|
33
|
+
const isLink = hasThread || (isText && state.availability === 'AVAILABLE')
|
|
34
|
+
const btnText = hasThread ? 'Show chat' : 'Start chat'
|
|
35
|
+
|
|
36
|
+
// Update container
|
|
37
|
+
container.innerHTML = nunjucks.render('availability.html', {
|
|
38
|
+
model: {
|
|
39
|
+
isText,
|
|
40
|
+
isLink,
|
|
41
|
+
availability: state.availability,
|
|
42
|
+
btnText,
|
|
43
|
+
unseen: state.unseen
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Conditionally reinstate focus after dom replacement
|
|
48
|
+
const link = container.querySelector('[data-wc-open-btn]')
|
|
49
|
+
const hasFocus = document.activeElement.hasAttribute('data-wc-open-btn')
|
|
50
|
+
if (hasFocus && link) {
|
|
51
|
+
link.focus()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
scroll (state) {
|
|
56
|
+
const container = this.container
|
|
57
|
+
const link = container.querySelector('[data-wc-link]')
|
|
58
|
+
if (!link) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Calculate offset
|
|
63
|
+
const rect = container.getBoundingClientRect()
|
|
64
|
+
const isBelowFold = rect.top + 35 > (window.innerHeight || document.documentElement.clientHeight)
|
|
65
|
+
|
|
66
|
+
// Toggle static/sticky display
|
|
67
|
+
const isHidden = (state.view === 'OPEN' || state.view === 'END') && !state.isOpen
|
|
68
|
+
const isFixed = isHidden && isBelowFold
|
|
69
|
+
document.documentElement.classList.toggle('wc-scroll-padding', isFixed)
|
|
70
|
+
document.body.classList.toggle('wc-scroll-padding', isFixed)
|
|
71
|
+
link.classList.toggle('wc-link--fixed', isFixed)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default Availability
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class Config {
|
|
2
|
+
static _breakpoints = { // Max width
|
|
3
|
+
mobile: '640px',
|
|
4
|
+
tablet: '834px'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
static maxQueue = 2
|
|
8
|
+
static timeout = -1 // 20 minutes
|
|
9
|
+
static countdown = -1 // Seconds
|
|
10
|
+
static poll = -1 // Seconds
|
|
11
|
+
|
|
12
|
+
static getBreakpoint (device) {
|
|
13
|
+
return this._breakpoints[device]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default Config
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
class Notification {
|
|
2
|
+
constructor () {
|
|
3
|
+
const url = '/public/sounds/notification.mp3'
|
|
4
|
+
this._url = url
|
|
5
|
+
|
|
6
|
+
// Create context
|
|
7
|
+
const context = new (window.AudioContext || window.webkitAudioContext)()
|
|
8
|
+
// context.addEventListener('statechange', () => {
|
|
9
|
+
// console.log('Audio: ', context.state)
|
|
10
|
+
// })
|
|
11
|
+
this._context = context
|
|
12
|
+
|
|
13
|
+
// Unlock audio
|
|
14
|
+
this._unlockAudioContext()
|
|
15
|
+
|
|
16
|
+
// Load buffer
|
|
17
|
+
this._init(url)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_init () {
|
|
21
|
+
const context = this._context
|
|
22
|
+
const url = this._url
|
|
23
|
+
|
|
24
|
+
fetch(url)
|
|
25
|
+
.then(response => response.arrayBuffer())
|
|
26
|
+
.then(buffer => context.decodeAudioData(buffer))
|
|
27
|
+
.then(decodedData => {
|
|
28
|
+
this._buffer = decodedData
|
|
29
|
+
})
|
|
30
|
+
.catch(console.error)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_unlockAudioContext () {
|
|
34
|
+
const context = this._context
|
|
35
|
+
if (context.state === 'suspended') {
|
|
36
|
+
const events = ['touchstart', 'touchend', 'mousedown', 'wheel', 'keydown', 'click']
|
|
37
|
+
const unlock = e => {
|
|
38
|
+
events.forEach(event => {
|
|
39
|
+
document.body.removeEventListener(event, unlock)
|
|
40
|
+
})
|
|
41
|
+
context.resume()
|
|
42
|
+
}
|
|
43
|
+
events.forEach(event => {
|
|
44
|
+
document.body.addEventListener(event, unlock, false)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
playSound () {
|
|
50
|
+
const context = this._context
|
|
51
|
+
const buffer = this._buffer
|
|
52
|
+
console.log('Play sound (context state: ', context.state, ')')
|
|
53
|
+
|
|
54
|
+
// Create source
|
|
55
|
+
const source = context.createBufferSource()
|
|
56
|
+
source.buffer = buffer
|
|
57
|
+
source.connect(context.destination)
|
|
58
|
+
source.start()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default Notification
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|