@qtoggle/qui 1.20.1 → 1.20.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.
@@ -138,31 +138,40 @@ class MultiStateSpritesIcon extends Icon {
138
138
  let bgWidth = params.bgWidth
139
139
  let bgHeight = params.bgHeight
140
140
 
141
+ let iconElement = element[0]
142
+
141
143
  /* 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'
144
+ let existingStateElements = []
145
+ for (let i = 0; i < iconElement.children.length; i++) {
146
+ let child = iconElement.children[i]
147
+ let isState = child.className.split(' ').some(function (c) {
148
+ return c !== 'qui-icon-decoration' && c.startsWith('qui-icon-') && c.length > 'qui-icon-'.length
145
149
  })
146
- })
150
+ if (isState) {
151
+ existingStateElements.push(child)
152
+ }
153
+ }
147
154
 
148
- let addedToDOM = element.parents('body').length
149
- if (addedToDOM) {
155
+ if (iconElement.isConnected) {
156
+ /* Only this branch fades the old elements out, and only it needs them as a jQuery object */
157
+ let oldElements = $(existingStateElements)
150
158
  asap(function () {
151
- existingStateElements.addClass('qui-icon-hidden')
159
+ oldElements.addClass('qui-icon-hidden')
152
160
  })
153
161
  Theme.afterTransition(function () {
154
- existingStateElements.remove()
155
- }, existingStateElements)
162
+ oldElements.remove()
163
+ }, oldElements)
156
164
  }
157
165
  else {
158
- existingStateElements.remove()
166
+ existingStateElements.forEach(e => e.remove())
159
167
  existingStateElements = []
160
168
  }
161
169
 
162
- element.addClass('qui-icon')
170
+ iconElement.classList.add('qui-icon')
163
171
 
164
- /* Add new icon state elements */
165
- let newElements = $()
172
+ /* Add new icon state elements. Built with plain DOM calls because this runs for every icon on screen, and
173
+ * jQuery's element, style and append helpers were about half of what renderTo cost on a 448-row list. */
174
+ let newElements = []
166
175
  ObjectUtils.forEach(this._states, function (state, details) {
167
176
 
168
177
  let offsetX = details.offsetX
@@ -173,35 +182,33 @@ class MultiStateSpritesIcon extends Icon {
173
182
  return
174
183
  }
175
184
 
176
- let stateDiv = $('<div></div>', {class: `qui-icon-${state}`})
185
+ let stateDiv = document.createElement('div')
186
+ stateDiv.className = `qui-icon-${state}`
177
187
  /* If no existing elements, display the new element directly, w/o any effect; otherwise start hidden and
178
188
  * do a transition to visible */
179
189
  if (existingStateElements.length) {
180
- stateDiv.addClass('qui-icon-hidden')
190
+ stateDiv.classList.add('qui-icon-hidden')
181
191
  }
182
192
 
183
- let css = {
184
- 'background-image': `url("${this._url}")`,
185
- 'background-position': `${(-offsetX * size)}${this._unit} ${(-offsetY * size)}${this._unit}`
186
- }
193
+ let style = stateDiv.style
194
+ style.backgroundImage = `url("${this._url}")`
195
+ style.backgroundPosition = `${(-offsetX * size)}${this._unit} ${(-offsetY * size)}${this._unit}`
187
196
  if (bgWidth && bgHeight) {
188
- css['background-size'] = `${bgWidth} ${bgHeight}`
197
+ style.backgroundSize = `${bgWidth} ${bgHeight}`
189
198
  }
190
199
 
191
200
  if (details.filter) {
192
- css['filter'] = details.filter
201
+ style.filter = details.filter
193
202
  }
194
203
 
195
- stateDiv.css(css)
196
-
197
- element.append(stateDiv)
198
- newElements = newElements.add(stateDiv)
204
+ iconElement.appendChild(stateDiv)
205
+ newElements.push(stateDiv)
199
206
 
200
207
  }, this)
201
208
 
202
209
  if (existingStateElements.length) {
203
210
  asap(function () {
204
- newElements.removeClass('qui-icon-hidden')
211
+ newElements.forEach(e => e.classList.remove('qui-icon-hidden'))
205
212
  })
206
213
  }
207
214
 
@@ -212,19 +219,23 @@ class MultiStateSpritesIcon extends Icon {
212
219
  }
213
220
  }, this)
214
221
 
215
- element.removeClass(this.constructor.KNOWN_STATES.map(s => `top-${s}`).join(' '))
216
- element.addClass(`top-${topState}`)
222
+ iconElement.classList.remove(...this.constructor.KNOWN_STATES.map(s => `top-${s}`))
223
+ iconElement.classList.add(`top-${topState}`)
217
224
 
218
225
  /* Decoration */
219
226
 
220
- let decorationDiv = element.children('div.qui-icon-decoration')
221
- if (!decorationDiv.length) {
222
- decorationDiv = null
227
+ let decorationDiv = null
228
+ for (let i = 0; i < iconElement.children.length; i++) {
229
+ if (iconElement.children[i].classList.contains('qui-icon-decoration')) {
230
+ decorationDiv = iconElement.children[i]
231
+ break
232
+ }
223
233
  }
224
234
 
225
235
  if (!decorationDiv && this._decoration) {
226
- decorationDiv = $('<div></div>', {class: 'qui-icon-decoration'})
227
- element.prepend(decorationDiv)
236
+ decorationDiv = document.createElement('div')
237
+ decorationDiv.className = 'qui-icon-decoration'
238
+ iconElement.insertBefore(decorationDiv, iconElement.firstChild)
228
239
  }
229
240
  else if (decorationDiv && !this._decoration) {
230
241
  decorationDiv.remove()
@@ -232,35 +243,42 @@ class MultiStateSpritesIcon extends Icon {
232
243
  }
233
244
 
234
245
  if (decorationDiv) {
235
- let css = {
236
- background: this._decoration
237
- }
246
+ decorationDiv.style.background = this._decoration
247
+
248
+ /* The decoration element outlives a render, so a colour lookup can resolve after another icon has been
249
+ * applied to the same element. Each render stamps the element, and only the latest one may write. */
250
+ let renderToken = (decorationDiv._quiRenderToken = (decorationDiv._quiRenderToken || 0) + 1)
238
251
 
239
252
  let bgColor = this._findBgColor(element)
240
253
  let bgRGB = Colors.str2rgba(bgColor)
241
254
  let decoRGB = Colors.str2rgba(this._decoration)
242
255
  if (Colors.contrast(bgRGB, decoRGB) > 1.5) {
243
- css['border-color'] = bgColor
256
+ decorationDiv.style.borderColor = bgColor
244
257
  }
245
258
  else {
246
259
  this._findIconColor(element).then(function (color) {
247
- css['border-color'] = color
260
+ if (decorationDiv._quiRenderToken === renderToken) {
261
+ decorationDiv.style.borderColor = color
262
+ }
248
263
  })
249
264
  }
250
-
251
- decorationDiv.css(css)
252
265
  }
253
266
  }
254
267
 
255
268
  _findBgColor(elem) {
256
269
  /* Find the background color behind the icon */
257
270
 
271
+ /* Icons are usually rendered before their row is attached, and a detached element has no computed style, so
272
+ * up such a chain css() only ever reports the inline one. Reading that directly skips a getComputedStyle call
273
+ * per element, worth about 130 ms across a 448-row list. */
274
+ let attached = elem.length > 0 && elem[0].isConnected
275
+
258
276
  let e = elem
259
277
  let bgColor = null
260
278
  while (e.length && e[0].tagName && (!bgColor || bgColor === 'rgba(0, 0, 0, 0)' ||
261
279
  bgColor === 'transparent')) {
262
280
 
263
- bgColor = e.css('background-color')
281
+ bgColor = attached ? e.css('background-color') : e[0].style.backgroundColor
264
282
  e = e.parent()
265
283
  }
266
284
 
package/js/utils/array.js CHANGED
@@ -2,10 +2,10 @@
2
2
  * @namespace qui.utils.array
3
3
  */
4
4
 
5
- function makeSortFunc(extractFunc, desc, thisArg) {
6
- return function (e1, e2) {
7
- let k1 = extractFunc.call(thisArg, e1)
8
- let k2 = extractFunc.call(thisArg, e2)
5
+ function makeKeySortFunc(desc) {
6
+ return function (p1, p2) {
7
+ let k1 = p1.key
8
+ let k2 = p2.key
9
9
 
10
10
  if ((desc && k1 > k2) || (!desc && k1 < k2)) {
11
11
  return -1
@@ -19,6 +19,36 @@ function makeSortFunc(extractFunc, desc, thisArg) {
19
19
  }
20
20
  }
21
21
 
22
+ /* The sort runs over element-key pairs, so the key function runs once per element instead of twice for every
23
+ * comparison the sort makes. Sorting 448 items in random order took 6408 key extractions; this brings it to 448. */
24
+ function sortPairs(array, func, desc, thisArg, sortFunc) {
25
+ /* Each element carries its own key instead of the two being matched up in a Map, which would file 0 and -0 under
26
+ * one entry. Undefined entries and holes are held back and restored at the end, as Array#sort leaves them. */
27
+ let length = array.length
28
+ let pairs = []
29
+ let undefinedCount = 0
30
+
31
+ array.forEach(function (e) {
32
+ if (e === undefined) {
33
+ undefinedCount++
34
+ }
35
+ else {
36
+ pairs.push({element: e, key: func.call(thisArg, e)})
37
+ }
38
+ })
39
+
40
+ sortFunc(pairs, makeKeySortFunc(desc))
41
+
42
+ array.length = 0
43
+ pairs.forEach(p => array.push(p.element))
44
+ while (undefinedCount--) {
45
+ array.push(undefined)
46
+ }
47
+ array.length = length
48
+
49
+ return array
50
+ }
51
+
22
52
  function merge(left, right, compareFunc) {
23
53
  let result = []
24
54
 
@@ -94,6 +124,9 @@ export function stableSort(array, compareFunc) {
94
124
 
95
125
  /**
96
126
  * Perform a sort on an array, *in place*, using a key extraction function.
127
+ *
128
+ * As with `Array#sort`, `undefined` entries and holes end up at the end of the array, in that order, and the key
129
+ * extraction function is not called for them. It is called once per remaining element.
97
130
  * @alias qui.utils.array.sortKey
98
131
  * @param {Array} array
99
132
  * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
@@ -103,11 +136,14 @@ export function stableSort(array, compareFunc) {
103
136
  * @returns {Array} the array
104
137
  */
105
138
  export function sortKey(array, func, desc = false, thisArg = null) {
106
- return array.sort(makeSortFunc(func, desc, thisArg))
139
+ return sortPairs(array, func, desc, thisArg, (pairs, compareFunc) => pairs.sort(compareFunc))
107
140
  }
108
141
 
109
142
  /**
110
143
  * Perform a stable sort on an array, *in place*, using a key extraction function.
144
+ *
145
+ * As with `Array#sort`, `undefined` entries and holes end up at the end of the array, in that order, and the key
146
+ * extraction function is not called for them. It is called once per remaining element.
111
147
  * @alias qui.utils.array.stableSortKey
112
148
  * @param {Array} array
113
149
  * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
@@ -117,7 +153,7 @@ export function sortKey(array, func, desc = false, thisArg = null) {
117
153
  * @returns {Array} the array
118
154
  */
119
155
  export function stableSortKey(array, func, desc = false, thisArg = null) {
120
- return stableSort(array, makeSortFunc(func, desc, thisArg))
156
+ return sortPairs(array, func, desc, thisArg, stableSort)
121
157
  }
122
158
 
123
159
  /**
@@ -40,28 +40,39 @@ const IconLabelViewMixin = Mixin((superclass = Object) => {
40
40
  * @returns {jQuery}
41
41
  */
42
42
  makeIconLabelContainer() {
43
- let container = $('<div></div>', {class: 'qui-icon-label-view'})
43
+ /* Built with plain DOM calls: five elements per view, and jQuery's element and append helpers were about
44
+ * a fifth of what the hub's 448-row port list cost to build. The three elements kept as fields stay
45
+ * jQuery objects, because the icon and label setters work on them. */
46
+ let container = document.createElement('div')
47
+ container.className = 'qui-icon-label-view'
44
48
 
45
49
  if (this._clickable) {
46
- container.addClass('qui-base-button')
50
+ container.classList.add('qui-base-button')
47
51
  }
48
52
 
49
- this._iconElement = $('<div></div>', {class: 'qui-icon'})
53
+ let iconElement = document.createElement('div')
54
+ iconElement.className = 'qui-icon'
55
+ this._iconElement = $(iconElement)
50
56
  this._applyIcon(this._icon, this._iconElement)
51
- container.append(this._iconElement)
57
+ container.appendChild(iconElement)
52
58
 
53
- let labelsContainer = $('<div></div>', {class: 'labels'})
54
- container.append(labelsContainer)
59
+ let labelsContainer = document.createElement('div')
60
+ labelsContainer.className = 'labels'
61
+ container.appendChild(labelsContainer)
55
62
 
56
- this._labelElement = $('<div></div>', {class: 'label'})
63
+ let labelElement = document.createElement('div')
64
+ labelElement.className = 'label'
65
+ this._labelElement = $(labelElement)
57
66
  this.setLabel(this._label)
58
- labelsContainer.append(this._labelElement)
67
+ labelsContainer.appendChild(labelElement)
59
68
 
60
- this._subLabelElement = $('<div></div>', {class: 'sub-label'})
69
+ let subLabelElement = document.createElement('div')
70
+ subLabelElement.className = 'sub-label'
71
+ this._subLabelElement = $(subLabelElement)
61
72
  this.setSubLabel(this._subLabel)
62
- labelsContainer.append(this._subLabelElement)
73
+ labelsContainer.appendChild(subLabelElement)
63
74
 
64
- return container
75
+ return $(container)
65
76
  }
66
77
 
67
78
  /**
@@ -113,23 +113,35 @@ $.widget('qui.progressdisk', $.qui.basewidget, {
113
113
  let iRadius = oRadius - thickness / 2
114
114
  let color = Theme.getColor(this.options.color)
115
115
 
116
- this.element.width(width)
117
- this.element.height(width)
118
- this.element.css('line-height', width)
119
-
120
- this._svg.setAttribute('width', width)
121
- this._svg.setAttribute('height', width)
122
- this._svg.setAttribute('viewBox', `0 0 ${fWidth} ${fWidth}`)
123
-
124
- this._ring.setAttribute('cx', oRadius.toString())
125
- this._ring.setAttribute('cy', oRadius.toString())
126
- this._ring.setAttribute('rx', iRadius.toString())
127
- this._ring.setAttribute('ry', iRadius.toString())
128
- this._ring.setAttribute('stroke-width', thickness.toString())
129
- this._ring.setAttribute('stroke', color)
130
-
131
- this._cursor.setAttribute('stroke-width', thickness.toString())
132
- this._cursor.setAttribute('stroke', color)
116
+ /* This runs on every value change, but the size only follows the radius option. jQuery's width() and height()
117
+ * each read the computed style, which came to about 400 ms while a 448-row list was being built. */
118
+ if (this._appliedWidth !== width) {
119
+ this._appliedWidth = width
120
+
121
+ this.element.width(width)
122
+ this.element.height(width)
123
+ this.element.css('line-height', width)
124
+
125
+ this._svg.setAttribute('width', width)
126
+ this._svg.setAttribute('height', width)
127
+ this._svg.setAttribute('viewBox', `0 0 ${fWidth} ${fWidth}`)
128
+
129
+ this._ring.setAttribute('cx', oRadius.toString())
130
+ this._ring.setAttribute('cy', oRadius.toString())
131
+ this._ring.setAttribute('rx', iRadius.toString())
132
+ this._ring.setAttribute('ry', iRadius.toString())
133
+ this._ring.setAttribute('stroke-width', thickness.toString())
134
+
135
+ this._cursor.setAttribute('stroke-width', thickness.toString())
136
+ }
137
+
138
+ /* The colour is still looked up every time, so a theme change lands on the next value. */
139
+ if (this._appliedColor !== color) {
140
+ this._appliedColor = color
141
+
142
+ this._ring.setAttribute('stroke', color)
143
+ this._cursor.setAttribute('stroke', color)
144
+ }
133
145
 
134
146
  if (this._curVal >= 0) {
135
147
  this._updateSVGPercent(iRadius, oRadius)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qtoggle/qui",
3
3
  "description": "A JavaScript UI library with batteries included.",
4
- "version": "1.20.1",
4
+ "version": "1.20.2",
5
5
  "author": {
6
6
  "name": "Calin Crisan",
7
7
  "email": "ccrisan@gmail.com"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qui-server"
3
- version = "1.20.1"
3
+ version = "1.20.2"
4
4
  description = "A fully fledged qToggle implementation written in Python"
5
5
  authors = [
6
6
  {name = "Calin Crisan", email = "ccrisan@gmail.com"},