@qtoggle/qui 0.0.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.
- package/.eslintignore +2 -0
- package/.eslintrc.json +492 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
- package/.github/workflows/main.yml +74 -0
- package/.pre-commit-config.yaml +8 -0
- package/LICENSE.txt +177 -0
- package/README.md +4 -0
- package/font/dejavusans-bold.woff +0 -0
- package/font/dejavusans-bolditalic.woff +0 -0
- package/font/dejavusans-italic.woff +0 -0
- package/font/dejavusans-regular.woff +0 -0
- package/img/qui-icons.svg +1937 -0
- package/js/base/base.js +47 -0
- package/js/base/condition-variable.js +92 -0
- package/js/base/errors.js +36 -0
- package/js/base/i18n.js +20 -0
- package/js/base/mixwith.js +135 -0
- package/js/base/require-js-compat.js +78 -0
- package/js/base/signal.js +91 -0
- package/js/base/singleton.js +66 -0
- package/js/base/timer.js +126 -0
- package/js/config.js +184 -0
- package/js/forms/common-fields/check-field.js +42 -0
- package/js/forms/common-fields/choice-buttons-field.js +30 -0
- package/js/forms/common-fields/color-combo-field.js +37 -0
- package/js/forms/common-fields/combo-field.js +108 -0
- package/js/forms/common-fields/common-fields.js +23 -0
- package/js/forms/common-fields/composite-field.js +132 -0
- package/js/forms/common-fields/custom-html-field.js +51 -0
- package/js/forms/common-fields/email-field.js +30 -0
- package/js/forms/common-fields/file-picker-field.js +46 -0
- package/js/forms/common-fields/jquery-ui-field.js +111 -0
- package/js/forms/common-fields/labels-field.js +69 -0
- package/js/forms/common-fields/numeric-field.js +39 -0
- package/js/forms/common-fields/password-field.js +28 -0
- package/js/forms/common-fields/phone-field.js +26 -0
- package/js/forms/common-fields/progress-disk-field.js +69 -0
- package/js/forms/common-fields/push-button-field.js +138 -0
- package/js/forms/common-fields/slider-field.js +51 -0
- package/js/forms/common-fields/text-area-field.js +34 -0
- package/js/forms/common-fields/text-field.js +89 -0
- package/js/forms/common-fields/up-down-field.js +85 -0
- package/js/forms/common-forms/common-forms.js +16 -0
- package/js/forms/common-forms/options-form.js +77 -0
- package/js/forms/common-forms/page-form.js +115 -0
- package/js/forms/form-button.js +202 -0
- package/js/forms/form-field.js +1183 -0
- package/js/forms/form.js +1181 -0
- package/js/forms/forms.js +68 -0
- package/js/global-glass.js +100 -0
- package/js/icons/default-stock.js +173 -0
- package/js/icons/icon.js +64 -0
- package/js/icons/icons.js +16 -0
- package/js/icons/multi-state-sprites-icon.js +362 -0
- package/js/icons/stock-icon.js +219 -0
- package/js/icons/stock.js +98 -0
- package/js/icons/stocks.js +57 -0
- package/js/index.js +232 -0
- package/js/lib/jquery.longpress.js +79 -0
- package/js/lib/jquery.module.js +4 -0
- package/js/lib/logger.module.js +4 -0
- package/js/lib/pep.module.js +4 -0
- package/js/lists/common-items/common-items.js +5 -0
- package/js/lists/common-items/icon-label-list-item.js +86 -0
- package/js/lists/common-lists/common-lists.js +5 -0
- package/js/lists/common-lists/page-list.js +53 -0
- package/js/lists/list-item.js +147 -0
- package/js/lists/list.js +636 -0
- package/js/lists/lists.js +26 -0
- package/js/main-ui/main-ui.js +64 -0
- package/js/main-ui/menu-bar.js +144 -0
- package/js/main-ui/options-bar.js +181 -0
- package/js/main-ui/status.js +185 -0
- package/js/main-ui/top-bar.js +59 -0
- package/js/messages/common-message-forms/common-message-forms.js +7 -0
- package/js/messages/common-message-forms/confirm-message-form.js +81 -0
- package/js/messages/common-message-forms/simple-message-form.js +67 -0
- package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
- package/js/messages/message-form.js +107 -0
- package/js/messages/messages.js +21 -0
- package/js/messages/sticky-modal-page.js +98 -0
- package/js/messages/sticky-modal-progress-message.js +27 -0
- package/js/messages/toast.js +164 -0
- package/js/navigation.js +654 -0
- package/js/pages/breadcrumbs.js +124 -0
- package/js/pages/common-pages/common-pages.js +6 -0
- package/js/pages/common-pages/modal-progress-page.js +83 -0
- package/js/pages/common-pages/structured-page.js +46 -0
- package/js/pages/page.js +1018 -0
- package/js/pages/pages-context.js +154 -0
- package/js/pages/pages.js +252 -0
- package/js/pwa.js +337 -0
- package/js/sections/section.js +612 -0
- package/js/sections/sections.js +300 -0
- package/js/tables/common-cells/common-cells.js +7 -0
- package/js/tables/common-cells/icon-label-table-cell.js +68 -0
- package/js/tables/common-cells/push-button-table-cell.js +133 -0
- package/js/tables/common-cells/simple-table-cell.js +37 -0
- package/js/tables/common-tables/common-tables.js +5 -0
- package/js/tables/common-tables/page-table.js +55 -0
- package/js/tables/table-cell.js +198 -0
- package/js/tables/table-row.js +126 -0
- package/js/tables/table.js +492 -0
- package/js/tables/tables.js +36 -0
- package/js/theme.js +304 -0
- package/js/utils/ajax.js +126 -0
- package/js/utils/array.js +194 -0
- package/js/utils/colors.js +445 -0
- package/js/utils/cookies.js +65 -0
- package/js/utils/crypto.js +439 -0
- package/js/utils/css.js +234 -0
- package/js/utils/date.js +300 -0
- package/js/utils/files.js +27 -0
- package/js/utils/gestures.js +165 -0
- package/js/utils/html.js +76 -0
- package/js/utils/misc.js +81 -0
- package/js/utils/object.js +324 -0
- package/js/utils/promise.js +49 -0
- package/js/utils/string.js +192 -0
- package/js/utils/url.js +187 -0
- package/js/utils/utils.js +3 -0
- package/js/utils/visibility-manager.js +211 -0
- package/js/views/common-views/common-views.js +7 -0
- package/js/views/common-views/icon-label-view.js +210 -0
- package/js/views/common-views/progress-view.js +89 -0
- package/js/views/common-views/structured-view.js +368 -0
- package/js/views/view.js +467 -0
- package/js/views/views.js +3 -0
- package/js/widgets/base-widget.js +23 -0
- package/js/widgets/common-widgets/check-button.js +109 -0
- package/js/widgets/common-widgets/choice-buttons.js +322 -0
- package/js/widgets/common-widgets/color-combo.js +104 -0
- package/js/widgets/common-widgets/combo.js +645 -0
- package/js/widgets/common-widgets/common-widgets.js +17 -0
- package/js/widgets/common-widgets/email-input.js +7 -0
- package/js/widgets/common-widgets/file-picker.js +133 -0
- package/js/widgets/common-widgets/labels.js +132 -0
- package/js/widgets/common-widgets/numeric-input.js +49 -0
- package/js/widgets/common-widgets/password-input.js +91 -0
- package/js/widgets/common-widgets/phone-input.js +7 -0
- package/js/widgets/common-widgets/progress-disk.js +174 -0
- package/js/widgets/common-widgets/push-button.js +155 -0
- package/js/widgets/common-widgets/slider.js +455 -0
- package/js/widgets/common-widgets/text-area.js +52 -0
- package/js/widgets/common-widgets/text-input.js +174 -0
- package/js/widgets/common-widgets/up-down.js +351 -0
- package/js/widgets/widgets.js +57 -0
- package/js/window.js +557 -0
- package/jsdoc.conf.json +20 -0
- package/less/base.less +123 -0
- package/less/forms/common-fields.less +101 -0
- package/less/forms/common-forms.less +5 -0
- package/less/forms/form-button.less +21 -0
- package/less/forms/form-field.less +266 -0
- package/less/forms/form.less +131 -0
- package/less/global-glass.less +64 -0
- package/less/icon-label-view.less +82 -0
- package/less/icons.less +144 -0
- package/less/lists.less +105 -0
- package/less/main-ui.less +328 -0
- package/less/messages.less +189 -0
- package/less/no-effects.less +24 -0
- package/less/pages/breadcrumbs.less +98 -0
- package/less/pages/common-pages.less +36 -0
- package/less/pages/page.less +70 -0
- package/less/progress-view.less +51 -0
- package/less/stock-icons.less +43 -0
- package/less/structured-view.less +245 -0
- package/less/tables.less +84 -0
- package/less/theme-dark.less +133 -0
- package/less/theme-light.less +132 -0
- package/less/theme.less +419 -0
- package/less/visibility-manager.less +11 -0
- package/less/widgets/check-button.less +96 -0
- package/less/widgets/choice-buttons.less +160 -0
- package/less/widgets/color-combo.less +33 -0
- package/less/widgets/combo.less +230 -0
- package/less/widgets/common-buttons.less +120 -0
- package/less/widgets/common.less +24 -0
- package/less/widgets/input.less +258 -0
- package/less/widgets/labels.less +81 -0
- package/less/widgets/progress-disk.less +70 -0
- package/less/widgets/slider.less +199 -0
- package/less/widgets/updown.less +115 -0
- package/less/widgets/various.less +36 -0
- package/package.json +47 -0
- package/pyproject.toml +45 -0
- package/qui/__init__.py +110 -0
- package/qui/constants.py +1 -0
- package/qui/exceptions.py +2 -0
- package/qui/j2template.py +71 -0
- package/qui/settings.py +60 -0
- package/qui/templates/manifest.json +25 -0
- package/qui/templates/qui.html +126 -0
- package/qui/templates/service-worker.js +188 -0
- package/qui/web/__init__.py +0 -0
- package/qui/web/tornado.py +220 -0
- package/scripts/postinstall.sh +10 -0
- package/webpack/webpack-adjust-css-urls-loader.js +36 -0
- package/webpack/webpack-common.js +384 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
|
|
2
|
+
import $ from '$qui/lib/jquery.module.js'
|
|
3
|
+
|
|
4
|
+
import * as Theme from '$qui/theme.js'
|
|
5
|
+
import * as ArrayUtils from '$qui/utils/array.js'
|
|
6
|
+
import * as Colors from '$qui/utils/colors.js'
|
|
7
|
+
import * as CSS from '$qui/utils/css.js'
|
|
8
|
+
import {asap} from '$qui/utils/misc.js'
|
|
9
|
+
import * as ObjectUtils from '$qui/utils/object.js'
|
|
10
|
+
import * as Window from '$qui/window.js'
|
|
11
|
+
|
|
12
|
+
import Icon from './icon.js'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} qui.icons.MultiStateSpritesIcon.StateDetails
|
|
17
|
+
* @property {Number} offsetX the X offset of the icon in the sprites image
|
|
18
|
+
* @property {Number} offsetY the Y offset of the icon in the sprites image
|
|
19
|
+
* @property {String} filter a CSS filter to apply to the icon
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An icon with different settings for various states.
|
|
24
|
+
*
|
|
25
|
+
* Defined icon states are `normal`, `active`, `focused` and `selected`.
|
|
26
|
+
*
|
|
27
|
+
* @alias qui.icons.MultiStateSpritesIcon
|
|
28
|
+
* @extends qui.icons.Icon
|
|
29
|
+
*/
|
|
30
|
+
class MultiStateSpritesIcon extends Icon {
|
|
31
|
+
|
|
32
|
+
static KNOWN_STATES = ['normal', 'active', 'focused', 'selected']
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @constructs
|
|
37
|
+
* @param {String} url the URL of the image resource
|
|
38
|
+
* @param {Number} bgWidth the total width of the image resource
|
|
39
|
+
* @param {Number} bgHeight the total height of the image resource
|
|
40
|
+
* @param {Number} [size] the size of the icon; defaults to `1`
|
|
41
|
+
* @param {String} [unit] the CSS unit used for all dimension attributes; defaults to `"rem"`
|
|
42
|
+
* @param {Object<String,qui.icons.MultiStateSpritesIcon.StateDetails>} [states] a mapping with icon states
|
|
43
|
+
* @param {Number} [scale] icon scaling factor; defaults to `1`
|
|
44
|
+
* @param {String} [decoration] icon decoration
|
|
45
|
+
* @param {...*} args parent class parameters
|
|
46
|
+
*/
|
|
47
|
+
constructor({
|
|
48
|
+
url,
|
|
49
|
+
bgWidth,
|
|
50
|
+
bgHeight,
|
|
51
|
+
size = 1,
|
|
52
|
+
unit = 'rem',
|
|
53
|
+
states = null,
|
|
54
|
+
scale = 1,
|
|
55
|
+
decoration = null,
|
|
56
|
+
...args
|
|
57
|
+
}) {
|
|
58
|
+
super({...args})
|
|
59
|
+
|
|
60
|
+
this._url = url
|
|
61
|
+
this._bgWidth = bgWidth
|
|
62
|
+
this._bgHeight = bgHeight
|
|
63
|
+
this._size = size
|
|
64
|
+
this._unit = unit
|
|
65
|
+
|
|
66
|
+
this._states = states
|
|
67
|
+
this._scale = scale
|
|
68
|
+
this._decoration = decoration
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Tell if the icon has a given state.
|
|
73
|
+
* @param {String} state
|
|
74
|
+
* @returns {Boolean}
|
|
75
|
+
*/
|
|
76
|
+
hasState(state) {
|
|
77
|
+
return state in this._states
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
toAttributes() {
|
|
81
|
+
return Object.assign(super.toAttributes(), {
|
|
82
|
+
url: this._url,
|
|
83
|
+
bgWidth: this._bgWidth,
|
|
84
|
+
bgHeight: this._bgHeight,
|
|
85
|
+
size: this._size,
|
|
86
|
+
unit: this._unit,
|
|
87
|
+
states: this._states,
|
|
88
|
+
scale: this._scale,
|
|
89
|
+
decoration: this._decoration
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_prepareParams() {
|
|
94
|
+
let m, u, v
|
|
95
|
+
let size = this._size
|
|
96
|
+
let bgWidth = String(this._bgWidth)
|
|
97
|
+
let bgHeight = String(this._bgHeight)
|
|
98
|
+
|
|
99
|
+
/* Perform required scaling transformations */
|
|
100
|
+
if (this._scale !== 1) {
|
|
101
|
+
size *= this._scale
|
|
102
|
+
|
|
103
|
+
m = bgWidth.match(new RegExp('[^\\d]'))
|
|
104
|
+
if (m) {
|
|
105
|
+
u = bgWidth.substring(m.index)
|
|
106
|
+
v = parseInt(bgWidth)
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
u = this._unit
|
|
110
|
+
v = Number(bgWidth) || 0
|
|
111
|
+
}
|
|
112
|
+
bgWidth = v * this._scale + u
|
|
113
|
+
|
|
114
|
+
m = bgHeight.match(new RegExp('[^\\d]'))
|
|
115
|
+
if (m) {
|
|
116
|
+
u = bgHeight.substring(m.index)
|
|
117
|
+
v = parseInt(bgHeight)
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
u = this._unit
|
|
121
|
+
v = Number(bgHeight) || 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bgHeight = v * this._scale + u
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
size: size,
|
|
129
|
+
bgWidth: bgWidth,
|
|
130
|
+
bgHeight: bgHeight
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
renderTo(element) {
|
|
135
|
+
let params = this._prepareParams()
|
|
136
|
+
|
|
137
|
+
let size = params.size
|
|
138
|
+
let bgWidth = params.bgWidth
|
|
139
|
+
let bgHeight = params.bgHeight
|
|
140
|
+
|
|
141
|
+
/* Find and remove existing icon state elements */
|
|
142
|
+
let existingStateElements = element.children().filter(function () {
|
|
143
|
+
return this.className.split(' ').some(function (c) {
|
|
144
|
+
return c.match(new RegExp('^qui-icon-[\\w\\-]+$')) && c !== 'qui-icon-decoration'
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
let addedToDOM = element.parents('body').length
|
|
149
|
+
if (addedToDOM) {
|
|
150
|
+
asap(function () {
|
|
151
|
+
existingStateElements.addClass('qui-icon-hidden')
|
|
152
|
+
})
|
|
153
|
+
Theme.afterTransition(function () {
|
|
154
|
+
existingStateElements.remove()
|
|
155
|
+
}, existingStateElements)
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
existingStateElements.remove()
|
|
159
|
+
existingStateElements = []
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
element.addClass('qui-icon')
|
|
163
|
+
|
|
164
|
+
/* Add new icon state elements */
|
|
165
|
+
let newElements = $()
|
|
166
|
+
ObjectUtils.forEach(this._states, function (state, details) {
|
|
167
|
+
|
|
168
|
+
let offsetX = details.offsetX
|
|
169
|
+
let offsetY = details.offsetY
|
|
170
|
+
|
|
171
|
+
/* Incomplete states don't get an element */
|
|
172
|
+
if (offsetX == null || offsetY == null) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let stateDiv = $('<div></div>', {class: `qui-icon-${state}`})
|
|
177
|
+
/* If no existing elements, display the new element directly, w/o any effect; otherwise start hidden and
|
|
178
|
+
* do a transition to visible */
|
|
179
|
+
if (existingStateElements.length) {
|
|
180
|
+
stateDiv.addClass('qui-icon-hidden')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let css = {
|
|
184
|
+
'background-image': `url("${this._url}")`,
|
|
185
|
+
'background-position': `${(-offsetX * size)}${this._unit} ${(-offsetY * size)}${this._unit}`
|
|
186
|
+
}
|
|
187
|
+
if (bgWidth && bgHeight) {
|
|
188
|
+
css['background-size'] = `${bgWidth} ${bgHeight}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (details.filter) {
|
|
192
|
+
css['filter'] = details.filter
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
stateDiv.css(css)
|
|
196
|
+
|
|
197
|
+
element.append(stateDiv)
|
|
198
|
+
newElements = newElements.add(stateDiv)
|
|
199
|
+
|
|
200
|
+
}, this)
|
|
201
|
+
|
|
202
|
+
if (existingStateElements.length) {
|
|
203
|
+
asap(function () {
|
|
204
|
+
newElements.removeClass('qui-icon-hidden')
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let topState = 'normal'
|
|
209
|
+
this.constructor.KNOWN_STATES.forEach(function (state) {
|
|
210
|
+
if (state in this._states) {
|
|
211
|
+
topState = state
|
|
212
|
+
}
|
|
213
|
+
}, this)
|
|
214
|
+
|
|
215
|
+
element.removeClass(this.constructor.KNOWN_STATES.map(s => `top-${s}`).join(' '))
|
|
216
|
+
element.addClass(`top-${topState}`)
|
|
217
|
+
|
|
218
|
+
/* Decoration */
|
|
219
|
+
|
|
220
|
+
let decorationDiv = element.children('div.qui-icon-decoration')
|
|
221
|
+
if (!decorationDiv.length) {
|
|
222
|
+
decorationDiv = null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!decorationDiv && this._decoration) {
|
|
226
|
+
decorationDiv = $('<div></div>', {class: 'qui-icon-decoration'})
|
|
227
|
+
element.prepend(decorationDiv)
|
|
228
|
+
}
|
|
229
|
+
else if (decorationDiv && !this._decoration) {
|
|
230
|
+
decorationDiv.remove()
|
|
231
|
+
decorationDiv = null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (decorationDiv) {
|
|
235
|
+
let css = {
|
|
236
|
+
background: this._decoration
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let bgColor = this._findBgColor(element)
|
|
240
|
+
let bgRGB = Colors.str2rgba(bgColor)
|
|
241
|
+
let decoRGB = Colors.str2rgba(this._decoration)
|
|
242
|
+
if (Colors.contrast(bgRGB, decoRGB) > 1.5) {
|
|
243
|
+
css['border-color'] = bgColor
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this._findIconColor(element).then(function (color) {
|
|
247
|
+
css['border-color'] = color
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
decorationDiv.css(css)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_findBgColor(elem) {
|
|
256
|
+
/* Find the background color behind the icon */
|
|
257
|
+
|
|
258
|
+
let e = elem
|
|
259
|
+
let bgColor = null
|
|
260
|
+
while (e.length && e[0].tagName && (!bgColor || bgColor === 'rgba(0, 0, 0, 0)' ||
|
|
261
|
+
bgColor === 'transparent')) {
|
|
262
|
+
|
|
263
|
+
bgColor = e.css('background-color')
|
|
264
|
+
e = e.parent()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!e.length || !e[0].tagName) {
|
|
268
|
+
/* Icon element not added yet to the DOM, or no element has a background color */
|
|
269
|
+
bgColor = Theme.getVar('background-color')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return bgColor
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_findIconColor(elem) {
|
|
276
|
+
/* Find the dominant color of the icon, using a canvas element */
|
|
277
|
+
|
|
278
|
+
return new Promise(function (resolve) {
|
|
279
|
+
let params = this._prepareParams()
|
|
280
|
+
|
|
281
|
+
let size = params.size
|
|
282
|
+
let pxFactor = 1
|
|
283
|
+
if (this._unit === 'em') {
|
|
284
|
+
if (Window.$body.has(elem).length) {
|
|
285
|
+
pxFactor = CSS.em2px(1, elem)
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
pxFactor = CSS.em2px(1)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (this._unit === 'rem') {
|
|
292
|
+
pxFactor = CSS.em2px(1)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let normalState = this._states['normal']
|
|
296
|
+
let offsetX = 0
|
|
297
|
+
let offsetY = 0
|
|
298
|
+
if (normalState) {
|
|
299
|
+
offsetX = normalState.offsetX || 0
|
|
300
|
+
offsetY = normalState.offsetY || 0
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
offsetX *= size
|
|
304
|
+
offsetY *= size
|
|
305
|
+
|
|
306
|
+
let canvas = document.createElement('canvas')
|
|
307
|
+
let context = canvas.getContext('2d')
|
|
308
|
+
|
|
309
|
+
let width = size * pxFactor
|
|
310
|
+
|
|
311
|
+
canvas.width = width
|
|
312
|
+
canvas.height = width /* Yes, width */
|
|
313
|
+
|
|
314
|
+
context.scale(this._scale, this._scale)
|
|
315
|
+
context.translate(-offsetX * pxFactor, -offsetY * pxFactor)
|
|
316
|
+
|
|
317
|
+
let img = new window.Image()
|
|
318
|
+
img.onload = function () {
|
|
319
|
+
|
|
320
|
+
context.drawImage(img, 0, 0)
|
|
321
|
+
let snapshot = context.getImageData(0, 0, width, width)
|
|
322
|
+
|
|
323
|
+
function getColor(x, y) {
|
|
324
|
+
let p = (y * width + x)
|
|
325
|
+
let r = snapshot.data[4 * p]
|
|
326
|
+
let g = snapshot.data[4 * p + 1]
|
|
327
|
+
let b = snapshot.data[4 * p + 2]
|
|
328
|
+
|
|
329
|
+
return Colors.rgba2str([r, g, b])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let bgColor = getColor(0, 0)
|
|
333
|
+
let colorDict = {}
|
|
334
|
+
|
|
335
|
+
for (let y = 0; y < width; y++) {
|
|
336
|
+
for (let x = 0; x < width; x++) {
|
|
337
|
+
let color = getColor(x, y)
|
|
338
|
+
if (color in colorDict) {
|
|
339
|
+
colorDict[color] += 1
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
colorDict[color] = 0
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let colorCounters = ArrayUtils.sortKey(Object.entries(colorDict), c => c[1], /* desc = */ true)
|
|
348
|
+
while (colorCounters[0][0] === bgColor && colorCounters.length > 1) {
|
|
349
|
+
colorCounters.shift()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
resolve(colorCounters[0][0])
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
img.src = this._url
|
|
356
|
+
}.bind(this))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
export default MultiStateSpritesIcon
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
|
|
2
|
+
import {AssertionError} from '$qui/base/errors.js'
|
|
3
|
+
import * as ObjectUtils from '$qui/utils/object.js'
|
|
4
|
+
|
|
5
|
+
import * as DefaultStock from './default-stock.js'
|
|
6
|
+
import Icon from './icon.js'
|
|
7
|
+
import MultiStateSpritesIcon from './multi-state-sprites-icon.js'
|
|
8
|
+
import * as Stocks from './stocks.js'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An icon defined by stock attributes.
|
|
13
|
+
* @alias qui.icons.StockIcon
|
|
14
|
+
* @extends qui.icons.Icon
|
|
15
|
+
*/
|
|
16
|
+
class StockIcon extends Icon {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @constructs
|
|
20
|
+
* @param {String} name the icon name (in normal state)
|
|
21
|
+
* @param {String} [stockName] the name of the stock (defaults to `qui`)
|
|
22
|
+
* @param {String} [variant] the icon variant (in normal state)
|
|
23
|
+
* @param {String} [activeName] the icon name in active state
|
|
24
|
+
* @param {String} [activeVariant] the icon variant in active state
|
|
25
|
+
* @param {String} [focusedName] the icon name in focused state
|
|
26
|
+
* @param {String} [focusedVariant] the icon variant in focused state
|
|
27
|
+
* @param {String} [selectedName] the icon name in selected state
|
|
28
|
+
* @param {String} [selectedVariant] the icon variant in selected state
|
|
29
|
+
* @param {Number} [scale] icon scaling factor (defaults to `1`)
|
|
30
|
+
* @param {String} [decoration] icon decoration
|
|
31
|
+
* @param {...*} args parent class parameters
|
|
32
|
+
*/
|
|
33
|
+
constructor({
|
|
34
|
+
name,
|
|
35
|
+
stockName = DefaultStock.NAME,
|
|
36
|
+
variant = null,
|
|
37
|
+
activeName = null,
|
|
38
|
+
activeVariant = null,
|
|
39
|
+
focusedName = null,
|
|
40
|
+
focusedVariant = null,
|
|
41
|
+
selectedName = null,
|
|
42
|
+
selectedVariant = null,
|
|
43
|
+
scale = 1,
|
|
44
|
+
decoration = null,
|
|
45
|
+
...args
|
|
46
|
+
}) {
|
|
47
|
+
super({...args})
|
|
48
|
+
|
|
49
|
+
this._name = name
|
|
50
|
+
this._stockName = stockName
|
|
51
|
+
this._variant = variant
|
|
52
|
+
this._activeName = activeName
|
|
53
|
+
this._activeVariant = activeVariant
|
|
54
|
+
this._focusedName = focusedName
|
|
55
|
+
this._focusedVariant = focusedVariant
|
|
56
|
+
this._selectedName = selectedName
|
|
57
|
+
this._selectedVariant = selectedVariant
|
|
58
|
+
this._scale = scale
|
|
59
|
+
this._decoration = decoration
|
|
60
|
+
|
|
61
|
+
/* A variant is specified but no corresponding name */
|
|
62
|
+
if (this._activeVariant && !this._activeName) {
|
|
63
|
+
this._activeName = this._name
|
|
64
|
+
}
|
|
65
|
+
if (this._focusedVariant && !this._focusedName) {
|
|
66
|
+
this._focusedName = this._name
|
|
67
|
+
}
|
|
68
|
+
if (this._selectedVariant && !this._selectedName) {
|
|
69
|
+
this._selectedName = this._name
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* A name is specified but no corresponding variant */
|
|
73
|
+
if (this._activeName && !this._activeVariant) {
|
|
74
|
+
this._activeVariant = this._variant
|
|
75
|
+
}
|
|
76
|
+
if (this._focusedName && !this._focusedVariant) {
|
|
77
|
+
this._focusedVariant = this._variant
|
|
78
|
+
}
|
|
79
|
+
if (this._selectedName && !this._selectedVariant) {
|
|
80
|
+
this._selectedVariant = this._variant
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toAttributes() {
|
|
85
|
+
return Object.assign(super.toAttributes(), {
|
|
86
|
+
name: this._name,
|
|
87
|
+
stockName: this._stockName,
|
|
88
|
+
variant: this._variant,
|
|
89
|
+
activeName: this._activeName,
|
|
90
|
+
activeVariant: this._activeVariant,
|
|
91
|
+
focusedName: this._focusedName,
|
|
92
|
+
focusedVariant: this._focusedVariant,
|
|
93
|
+
selectedName: this._selectedName,
|
|
94
|
+
selectedVariant: this._selectedVariant,
|
|
95
|
+
scale: this._scale,
|
|
96
|
+
decoration: this._decoration
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return a dictionary with attributes suitable to build a {@link qui.icons.MultiStateSpritesIcon}.
|
|
102
|
+
* @returns {?qui.icons.MultiStateSpritesIcon}
|
|
103
|
+
*/
|
|
104
|
+
toMultiStateSpritesIcon() {
|
|
105
|
+
let stock = Stocks.get(this._stockName)
|
|
106
|
+
if (!stock) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let attributes = Object.assign(super.toAttributes(), {
|
|
111
|
+
url: stock.src,
|
|
112
|
+
bgWidth: stock.width,
|
|
113
|
+
bgHeight: stock.height,
|
|
114
|
+
size: stock.size,
|
|
115
|
+
unit: stock.unit
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
let states = {}
|
|
119
|
+
let filterVariant
|
|
120
|
+
|
|
121
|
+
if (this._name) {
|
|
122
|
+
filterVariant = stock.parseVariant(this._variant)
|
|
123
|
+
states['normal'] = {
|
|
124
|
+
offsetX: stock.names[this._name],
|
|
125
|
+
offsetY: stock.variants[filterVariant.variant],
|
|
126
|
+
filter: filterVariant.filter
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this._activeName) {
|
|
131
|
+
filterVariant = stock.parseVariant(this._activeVariant)
|
|
132
|
+
states['active'] = {
|
|
133
|
+
offsetX: stock.names[this._activeName],
|
|
134
|
+
offsetY: stock.variants[filterVariant.variant],
|
|
135
|
+
filter: filterVariant.filter
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this._focusedName) {
|
|
140
|
+
filterVariant = stock.parseVariant(this._focusedVariant)
|
|
141
|
+
states['focused'] = {
|
|
142
|
+
offsetX: stock.names[this._focusedName],
|
|
143
|
+
offsetY: stock.variants[filterVariant.variant],
|
|
144
|
+
filter: filterVariant.filter
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this._selectedName) {
|
|
149
|
+
filterVariant = stock.parseVariant(this._selectedVariant)
|
|
150
|
+
states['selected'] = {
|
|
151
|
+
offsetX: stock.names[this._selectedName],
|
|
152
|
+
offsetY: stock.variants[filterVariant.variant],
|
|
153
|
+
filter: filterVariant.filter
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
attributes.states = states
|
|
158
|
+
|
|
159
|
+
if (this._scale) {
|
|
160
|
+
attributes.scale = this._scale
|
|
161
|
+
}
|
|
162
|
+
if (this._decoration) {
|
|
163
|
+
attributes.decoration = this._decoration
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return new MultiStateSpritesIcon(attributes)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Return an updated version of the icon by altering some of the attributes.
|
|
171
|
+
* @param {Object} attributes the attributes to alter
|
|
172
|
+
* @returns qui.icons.StockIcon
|
|
173
|
+
*/
|
|
174
|
+
alter(attributes) {
|
|
175
|
+
return new this.constructor(Object.assign(this.toAttributes(), attributes))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Return an updated version of the icon by altering some of the attributes only if they are unset (`null`).
|
|
180
|
+
*
|
|
181
|
+
* This method allows creating an icon by providing default attribute values.
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} attributes the attributes to alter
|
|
184
|
+
* @returns qui.icons.StockIcon
|
|
185
|
+
*/
|
|
186
|
+
alterDefault(attributes) {
|
|
187
|
+
/* Keep only attributes whose value is currently null */
|
|
188
|
+
attributes = ObjectUtils.filter(attributes, (key) => (this[`_${key}`] == null))
|
|
189
|
+
|
|
190
|
+
return this.alter(attributes)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Alter a stock icon that has already been applied to an element, in place.
|
|
195
|
+
* @param {jQuery} element
|
|
196
|
+
* @param {Object} attributes the attributes to alter
|
|
197
|
+
*/
|
|
198
|
+
static alterElement(element, attributes) {
|
|
199
|
+
let icon = Icon.getFromElement(element)
|
|
200
|
+
if (!icon) {
|
|
201
|
+
throw new AssertionError('Attempt to alter element without icon')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!(icon instanceof StockIcon)) {
|
|
205
|
+
throw new AssertionError('Attempt to alter an icon that is not StockIcon')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
icon = icon.alter(attributes)
|
|
209
|
+
icon.applyTo(element)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
renderTo(element) {
|
|
213
|
+
this.toMultiStateSpritesIcon().applyTo(element)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
export default StockIcon
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
import {appendBuildHash} from '$qui/utils/misc.js'
|
|
3
|
+
|
|
4
|
+
import * as DefaultStock from './default-stock.js'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An icon stock.
|
|
9
|
+
* @alias qui.icons.Stock
|
|
10
|
+
*/
|
|
11
|
+
class Stock {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @constructs
|
|
15
|
+
* @param {String} src image source (normally a URL)
|
|
16
|
+
* @param {String} unit unit of measurement for icon size (e.g. `"em"`, `"rem"`, `"px"`)
|
|
17
|
+
* @param {Number} size icon size with respect to the `unit` (the icon is assumed to be a square)
|
|
18
|
+
* @param {Number} width the number of icons in a row present in the source
|
|
19
|
+
* @param {Number} height the number of icon rows present in the source
|
|
20
|
+
* @param {Object<String,Number>} names a mapping associating each icon name to its horizontal offset
|
|
21
|
+
* @param {?Object<String,Number>} [variants] a mapping associating each icon variant to its vertical offset; the
|
|
22
|
+
* variants of the default stock ({@link qui.icons.defaultstock} are used if not supplied
|
|
23
|
+
* @param {Object<String,String>} [variantAliases] an optional mapping of aliases to icon variants; the variant
|
|
24
|
+
* aliases of the default stock ({@link qui.icons.defaultstock} are used if not supplied
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
constructor({src, unit, size, width, height, names, variants = null, variantAliases = null}) {
|
|
28
|
+
src = appendBuildHash(src)
|
|
29
|
+
|
|
30
|
+
this.src = src
|
|
31
|
+
this.unit = unit
|
|
32
|
+
this.size = size
|
|
33
|
+
this.width = width
|
|
34
|
+
this.height = height
|
|
35
|
+
this.names = names
|
|
36
|
+
this.variants = variants
|
|
37
|
+
this.variantAliases = variantAliases
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Make the stock ready to be used by ensuring all its properties are prepared.
|
|
42
|
+
*/
|
|
43
|
+
prepare() {
|
|
44
|
+
/* Copy variants from default stock */
|
|
45
|
+
let defaultStock = DefaultStock.get()
|
|
46
|
+
|
|
47
|
+
if (!this.variants) {
|
|
48
|
+
this.variants = defaultStock.variants
|
|
49
|
+
}
|
|
50
|
+
if (!this.variantAliases) {
|
|
51
|
+
this.variantAliases = defaultStock.variantAliases
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transform a variant into its corresponding filter and final variant.
|
|
57
|
+
* @param {String} variant
|
|
58
|
+
* @returns {{filter: String, variant: String}}
|
|
59
|
+
*/
|
|
60
|
+
parseVariant(variant) {
|
|
61
|
+
/* First resolve any alias */
|
|
62
|
+
let c = 0
|
|
63
|
+
while (variant in this.variantAliases) {
|
|
64
|
+
variant = this.variantAliases[variant]
|
|
65
|
+
|
|
66
|
+
/* Prevent loops; allow at most 10 levels of aliasing */
|
|
67
|
+
c++
|
|
68
|
+
if (c > 10) {
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!variant) {
|
|
74
|
+
return {
|
|
75
|
+
variant: null,
|
|
76
|
+
filter: null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let parts = variant.split(' ')
|
|
81
|
+
if (parts.length > 1) {
|
|
82
|
+
return {
|
|
83
|
+
variant: parts[0],
|
|
84
|
+
filter: parts.slice(1).join(' ')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return {
|
|
89
|
+
variant: variant,
|
|
90
|
+
filter: null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
export default Stock
|