@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.
Files changed (202) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +492 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
  6. package/.github/workflows/main.yml +74 -0
  7. package/.pre-commit-config.yaml +8 -0
  8. package/LICENSE.txt +177 -0
  9. package/README.md +4 -0
  10. package/font/dejavusans-bold.woff +0 -0
  11. package/font/dejavusans-bolditalic.woff +0 -0
  12. package/font/dejavusans-italic.woff +0 -0
  13. package/font/dejavusans-regular.woff +0 -0
  14. package/img/qui-icons.svg +1937 -0
  15. package/js/base/base.js +47 -0
  16. package/js/base/condition-variable.js +92 -0
  17. package/js/base/errors.js +36 -0
  18. package/js/base/i18n.js +20 -0
  19. package/js/base/mixwith.js +135 -0
  20. package/js/base/require-js-compat.js +78 -0
  21. package/js/base/signal.js +91 -0
  22. package/js/base/singleton.js +66 -0
  23. package/js/base/timer.js +126 -0
  24. package/js/config.js +184 -0
  25. package/js/forms/common-fields/check-field.js +42 -0
  26. package/js/forms/common-fields/choice-buttons-field.js +30 -0
  27. package/js/forms/common-fields/color-combo-field.js +37 -0
  28. package/js/forms/common-fields/combo-field.js +108 -0
  29. package/js/forms/common-fields/common-fields.js +23 -0
  30. package/js/forms/common-fields/composite-field.js +132 -0
  31. package/js/forms/common-fields/custom-html-field.js +51 -0
  32. package/js/forms/common-fields/email-field.js +30 -0
  33. package/js/forms/common-fields/file-picker-field.js +46 -0
  34. package/js/forms/common-fields/jquery-ui-field.js +111 -0
  35. package/js/forms/common-fields/labels-field.js +69 -0
  36. package/js/forms/common-fields/numeric-field.js +39 -0
  37. package/js/forms/common-fields/password-field.js +28 -0
  38. package/js/forms/common-fields/phone-field.js +26 -0
  39. package/js/forms/common-fields/progress-disk-field.js +69 -0
  40. package/js/forms/common-fields/push-button-field.js +138 -0
  41. package/js/forms/common-fields/slider-field.js +51 -0
  42. package/js/forms/common-fields/text-area-field.js +34 -0
  43. package/js/forms/common-fields/text-field.js +89 -0
  44. package/js/forms/common-fields/up-down-field.js +85 -0
  45. package/js/forms/common-forms/common-forms.js +16 -0
  46. package/js/forms/common-forms/options-form.js +77 -0
  47. package/js/forms/common-forms/page-form.js +115 -0
  48. package/js/forms/form-button.js +202 -0
  49. package/js/forms/form-field.js +1183 -0
  50. package/js/forms/form.js +1181 -0
  51. package/js/forms/forms.js +68 -0
  52. package/js/global-glass.js +100 -0
  53. package/js/icons/default-stock.js +173 -0
  54. package/js/icons/icon.js +64 -0
  55. package/js/icons/icons.js +16 -0
  56. package/js/icons/multi-state-sprites-icon.js +362 -0
  57. package/js/icons/stock-icon.js +219 -0
  58. package/js/icons/stock.js +98 -0
  59. package/js/icons/stocks.js +57 -0
  60. package/js/index.js +232 -0
  61. package/js/lib/jquery.longpress.js +79 -0
  62. package/js/lib/jquery.module.js +4 -0
  63. package/js/lib/logger.module.js +4 -0
  64. package/js/lib/pep.module.js +4 -0
  65. package/js/lists/common-items/common-items.js +5 -0
  66. package/js/lists/common-items/icon-label-list-item.js +86 -0
  67. package/js/lists/common-lists/common-lists.js +5 -0
  68. package/js/lists/common-lists/page-list.js +53 -0
  69. package/js/lists/list-item.js +147 -0
  70. package/js/lists/list.js +636 -0
  71. package/js/lists/lists.js +26 -0
  72. package/js/main-ui/main-ui.js +64 -0
  73. package/js/main-ui/menu-bar.js +144 -0
  74. package/js/main-ui/options-bar.js +181 -0
  75. package/js/main-ui/status.js +185 -0
  76. package/js/main-ui/top-bar.js +59 -0
  77. package/js/messages/common-message-forms/common-message-forms.js +7 -0
  78. package/js/messages/common-message-forms/confirm-message-form.js +81 -0
  79. package/js/messages/common-message-forms/simple-message-form.js +67 -0
  80. package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
  81. package/js/messages/message-form.js +107 -0
  82. package/js/messages/messages.js +21 -0
  83. package/js/messages/sticky-modal-page.js +98 -0
  84. package/js/messages/sticky-modal-progress-message.js +27 -0
  85. package/js/messages/toast.js +164 -0
  86. package/js/navigation.js +654 -0
  87. package/js/pages/breadcrumbs.js +124 -0
  88. package/js/pages/common-pages/common-pages.js +6 -0
  89. package/js/pages/common-pages/modal-progress-page.js +83 -0
  90. package/js/pages/common-pages/structured-page.js +46 -0
  91. package/js/pages/page.js +1018 -0
  92. package/js/pages/pages-context.js +154 -0
  93. package/js/pages/pages.js +252 -0
  94. package/js/pwa.js +337 -0
  95. package/js/sections/section.js +612 -0
  96. package/js/sections/sections.js +300 -0
  97. package/js/tables/common-cells/common-cells.js +7 -0
  98. package/js/tables/common-cells/icon-label-table-cell.js +68 -0
  99. package/js/tables/common-cells/push-button-table-cell.js +133 -0
  100. package/js/tables/common-cells/simple-table-cell.js +37 -0
  101. package/js/tables/common-tables/common-tables.js +5 -0
  102. package/js/tables/common-tables/page-table.js +55 -0
  103. package/js/tables/table-cell.js +198 -0
  104. package/js/tables/table-row.js +126 -0
  105. package/js/tables/table.js +492 -0
  106. package/js/tables/tables.js +36 -0
  107. package/js/theme.js +304 -0
  108. package/js/utils/ajax.js +126 -0
  109. package/js/utils/array.js +194 -0
  110. package/js/utils/colors.js +445 -0
  111. package/js/utils/cookies.js +65 -0
  112. package/js/utils/crypto.js +439 -0
  113. package/js/utils/css.js +234 -0
  114. package/js/utils/date.js +300 -0
  115. package/js/utils/files.js +27 -0
  116. package/js/utils/gestures.js +165 -0
  117. package/js/utils/html.js +76 -0
  118. package/js/utils/misc.js +81 -0
  119. package/js/utils/object.js +324 -0
  120. package/js/utils/promise.js +49 -0
  121. package/js/utils/string.js +192 -0
  122. package/js/utils/url.js +187 -0
  123. package/js/utils/utils.js +3 -0
  124. package/js/utils/visibility-manager.js +211 -0
  125. package/js/views/common-views/common-views.js +7 -0
  126. package/js/views/common-views/icon-label-view.js +210 -0
  127. package/js/views/common-views/progress-view.js +89 -0
  128. package/js/views/common-views/structured-view.js +368 -0
  129. package/js/views/view.js +467 -0
  130. package/js/views/views.js +3 -0
  131. package/js/widgets/base-widget.js +23 -0
  132. package/js/widgets/common-widgets/check-button.js +109 -0
  133. package/js/widgets/common-widgets/choice-buttons.js +322 -0
  134. package/js/widgets/common-widgets/color-combo.js +104 -0
  135. package/js/widgets/common-widgets/combo.js +645 -0
  136. package/js/widgets/common-widgets/common-widgets.js +17 -0
  137. package/js/widgets/common-widgets/email-input.js +7 -0
  138. package/js/widgets/common-widgets/file-picker.js +133 -0
  139. package/js/widgets/common-widgets/labels.js +132 -0
  140. package/js/widgets/common-widgets/numeric-input.js +49 -0
  141. package/js/widgets/common-widgets/password-input.js +91 -0
  142. package/js/widgets/common-widgets/phone-input.js +7 -0
  143. package/js/widgets/common-widgets/progress-disk.js +174 -0
  144. package/js/widgets/common-widgets/push-button.js +155 -0
  145. package/js/widgets/common-widgets/slider.js +455 -0
  146. package/js/widgets/common-widgets/text-area.js +52 -0
  147. package/js/widgets/common-widgets/text-input.js +174 -0
  148. package/js/widgets/common-widgets/up-down.js +351 -0
  149. package/js/widgets/widgets.js +57 -0
  150. package/js/window.js +557 -0
  151. package/jsdoc.conf.json +20 -0
  152. package/less/base.less +123 -0
  153. package/less/forms/common-fields.less +101 -0
  154. package/less/forms/common-forms.less +5 -0
  155. package/less/forms/form-button.less +21 -0
  156. package/less/forms/form-field.less +266 -0
  157. package/less/forms/form.less +131 -0
  158. package/less/global-glass.less +64 -0
  159. package/less/icon-label-view.less +82 -0
  160. package/less/icons.less +144 -0
  161. package/less/lists.less +105 -0
  162. package/less/main-ui.less +328 -0
  163. package/less/messages.less +189 -0
  164. package/less/no-effects.less +24 -0
  165. package/less/pages/breadcrumbs.less +98 -0
  166. package/less/pages/common-pages.less +36 -0
  167. package/less/pages/page.less +70 -0
  168. package/less/progress-view.less +51 -0
  169. package/less/stock-icons.less +43 -0
  170. package/less/structured-view.less +245 -0
  171. package/less/tables.less +84 -0
  172. package/less/theme-dark.less +133 -0
  173. package/less/theme-light.less +132 -0
  174. package/less/theme.less +419 -0
  175. package/less/visibility-manager.less +11 -0
  176. package/less/widgets/check-button.less +96 -0
  177. package/less/widgets/choice-buttons.less +160 -0
  178. package/less/widgets/color-combo.less +33 -0
  179. package/less/widgets/combo.less +230 -0
  180. package/less/widgets/common-buttons.less +120 -0
  181. package/less/widgets/common.less +24 -0
  182. package/less/widgets/input.less +258 -0
  183. package/less/widgets/labels.less +81 -0
  184. package/less/widgets/progress-disk.less +70 -0
  185. package/less/widgets/slider.less +199 -0
  186. package/less/widgets/updown.less +115 -0
  187. package/less/widgets/various.less +36 -0
  188. package/package.json +47 -0
  189. package/pyproject.toml +45 -0
  190. package/qui/__init__.py +110 -0
  191. package/qui/constants.py +1 -0
  192. package/qui/exceptions.py +2 -0
  193. package/qui/j2template.py +71 -0
  194. package/qui/settings.py +60 -0
  195. package/qui/templates/manifest.json +25 -0
  196. package/qui/templates/qui.html +126 -0
  197. package/qui/templates/service-worker.js +188 -0
  198. package/qui/web/__init__.py +0 -0
  199. package/qui/web/tornado.py +220 -0
  200. package/scripts/postinstall.sh +10 -0
  201. package/webpack/webpack-adjust-css-urls-loader.js +36 -0
  202. 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