@qbix/q 1.0.5
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/LICENSE +21 -0
- package/README.md +678 -0
- package/dist/Metrics.js +2873 -0
- package/dist/Metrics.min.js +95 -0
- package/dist/Q.js +15677 -0
- package/dist/Q.min.js +385 -0
- package/dist/Q.minimal.js +11594 -0
- package/dist/Q.minimal.min.js +286 -0
- package/dist/handlebars-v4.0.10.min.js +29 -0
- package/dist/handlebars.minimal.min.js +1 -0
- package/dist/img/hints/rotate-left.gif +0 -0
- package/dist/img/hints/swipe-down.gif +0 -0
- package/dist/img/hints/swipe-up.gif +0 -0
- package/dist/img/hints/tap.gif +0 -0
- package/dist/img/throbbers/loading.gif +0 -0
- package/dist/jquery.minimal.min.js +18 -0
- package/dist/methods/Q/Audio/load.js +29 -0
- package/dist/methods/Q/Audio/loadVoices.js +36 -0
- package/dist/methods/Q/Audio/play.js +47 -0
- package/dist/methods/Q/Audio/speak.js +149 -0
- package/dist/methods/Q/Crypto/delegate.js +186 -0
- package/dist/methods/Q/Crypto/internalKeypair.js +170 -0
- package/dist/methods/Q/Crypto/sign.js +200 -0
- package/dist/methods/Q/Crypto/verify.js +212 -0
- package/dist/methods/Q/Crypto/verifyDelegated.js +214 -0
- package/dist/methods/Q/Data/Bloom/_internal.js +163 -0
- package/dist/methods/Q/Data/Bloom/create.js +23 -0
- package/dist/methods/Q/Data/Bloom/fromBase64.js +21 -0
- package/dist/methods/Q/Data/Bloom/fromBytes.js +15 -0
- package/dist/methods/Q/Data/Bloom/fromElements.js +33 -0
- package/dist/methods/Q/Data/Merkle/_internal.js +68 -0
- package/dist/methods/Q/Data/Merkle/build.js +29 -0
- package/dist/methods/Q/Data/Merkle/proof.js +50 -0
- package/dist/methods/Q/Data/Merkle/verify.js +32 -0
- package/dist/methods/Q/Data/Prolly/_internal.js +190 -0
- package/dist/methods/Q/Data/Prolly/build.js +28 -0
- package/dist/methods/Q/Data/Prolly/delete.js +28 -0
- package/dist/methods/Q/Data/Prolly/diff.js +70 -0
- package/dist/methods/Q/Data/Prolly/get.js +38 -0
- package/dist/methods/Q/Data/Prolly/set.js +32 -0
- package/dist/methods/Q/Data/compress.js +45 -0
- package/dist/methods/Q/Data/decompress.js +35 -0
- package/dist/methods/Q/Data/decrypt.js +55 -0
- package/dist/methods/Q/Data/derive.js +76 -0
- package/dist/methods/Q/Data/digest.js +29 -0
- package/dist/methods/Q/Data/encrypt.js +61 -0
- package/dist/methods/Q/Data/generateKey.js +40 -0
- package/dist/methods/Q/Data/hkdf.js +44 -0
- package/dist/methods/Q/Data/importKey.js +34 -0
- package/dist/methods/Q/Data/sign.js +42 -0
- package/dist/methods/Q/Data/verify.js +45 -0
- package/dist/methods/Q/Onboarding/handle.js +50 -0
- package/dist/methods/Q/Onboarding/start.js +165 -0
- package/dist/methods/Q/Onboarding/stop.js +21 -0
- package/dist/methods/Q/Sandbox/run.js +392 -0
- package/dist/methods/Q/Tool/define/component.js +218 -0
- package/dist/methods/Q/globalMemoryWalk.js +82 -0
- package/dist/methods/Q/leaves.js +34 -0
- package/dist/methods/Q/registerWebComponent.js +0 -0
- package/dist/methods/Q/sanitize.js +142 -0
- package/dist/test.html +6 -0
- package/dist/tools/Q/lazyload.js +433 -0
- package/package.json +26 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
/**
|
|
3
|
+
* Q plugin's front end code
|
|
4
|
+
*
|
|
5
|
+
* @module Q
|
|
6
|
+
* @class Q
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Traverse all the leaves and optionally modify the values
|
|
11
|
+
* @static
|
|
12
|
+
* @method leaves
|
|
13
|
+
* @param {Object|Array|mixed} structure
|
|
14
|
+
* @param {Function} callback This will be called for every leaf.
|
|
15
|
+
* It receives the current value of the leaf, and must return a value
|
|
16
|
+
* that will be set there (to skip changes, simply return the current value)
|
|
17
|
+
* @return {mixed} the first parameter passed, which was modified during the loop
|
|
18
|
+
*/
|
|
19
|
+
Q.leaves = function Q_leaves(structure, callback) {
|
|
20
|
+
if (Q.isArrayLike(structure)) {
|
|
21
|
+
for (var i=0, l=structure.length; i<l; ++i) {
|
|
22
|
+
structure[i] = Q.leaves(structure[i], callback);
|
|
23
|
+
}
|
|
24
|
+
} else if (typeof structure === 'object') {
|
|
25
|
+
for (var k in structure) {
|
|
26
|
+
structure[k] = Q.leaves(structure[k], callback);
|
|
27
|
+
}
|
|
28
|
+
} else { // we found a scalar leaf
|
|
29
|
+
structure = callback(structure);
|
|
30
|
+
}
|
|
31
|
+
return structure;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
/**
|
|
3
|
+
* Q plugin's front end code
|
|
4
|
+
*
|
|
5
|
+
* @module Q
|
|
6
|
+
* @class Q
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes a DOM element and removes potentially dangerous tags and attributes.
|
|
11
|
+
* Use this to clean user-provided HTML before inserting it into the DOM.
|
|
12
|
+
*
|
|
13
|
+
* @static
|
|
14
|
+
* @method sanitize
|
|
15
|
+
* @param {HTMLElement} container A DOM element (e.g. div) containing the HTML to sanitize.
|
|
16
|
+
* @param {Function} callback A function to call after sanitization is complete.
|
|
17
|
+
* It receives the sanitized container as its only argument.
|
|
18
|
+
* @param {Object} [options] Optional configuration:
|
|
19
|
+
* @param {Array} [options.allowedSrc] Allowed URL prefixes for `src` attributes.
|
|
20
|
+
* @param {Array} [options.allowedHref] Allowed URL prefixes for `href` attributes.
|
|
21
|
+
* @param {Array} [options.preserveAttributes] Attributes that should not be removed.
|
|
22
|
+
* @param {Array} [options.dangerousAttributes] Additional dangerous attributes to remove.
|
|
23
|
+
* @param {Boolean} [options.createShadowRoot] Whether this will be used inside a shadow DOM.
|
|
24
|
+
*/
|
|
25
|
+
Q.sanitize = function Q_sanitize(container, callback, options) {
|
|
26
|
+
var dangerousTags = ['script', 'iframe', 'object', 'embed', 'svg', 'math', 'base', 'link'];
|
|
27
|
+
var allowedSrc = (options && options.allowedSrc) || ['data:image/', 'https://', 'http://'];
|
|
28
|
+
var allowedHref = (options && options.allowedHref) || [];
|
|
29
|
+
var preserveAttributes = (options && options.preserveAttributes) || [];
|
|
30
|
+
var customDangerousAttributes = (options && options.dangerousAttributes) || [];
|
|
31
|
+
|
|
32
|
+
var dangerousAttributes = [
|
|
33
|
+
'srcdoc', 'sandbox', 'allowfullscreen', 'autofocus'
|
|
34
|
+
].concat(customDangerousAttributes);
|
|
35
|
+
|
|
36
|
+
var all = container.querySelectorAll('*');
|
|
37
|
+
|
|
38
|
+
for (var i = 0; i < all.length; i++) {
|
|
39
|
+
var element = all[i];
|
|
40
|
+
var tagName = element.tagName.toLowerCase();
|
|
41
|
+
var attrs = element.attributes;
|
|
42
|
+
|
|
43
|
+
// Remove entire element if it's a dangerous tag
|
|
44
|
+
if (dangerousTags.includes(tagName)) {
|
|
45
|
+
if (element.parentNode) {
|
|
46
|
+
element.parentNode.removeChild(element);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (var j = attrs.length - 1; j >= 0; j--) {
|
|
52
|
+
var attr = attrs[j];
|
|
53
|
+
var name = attr.name.toLowerCase();
|
|
54
|
+
var value = (attr.value || '').trim();
|
|
55
|
+
var shouldRemove = false;
|
|
56
|
+
|
|
57
|
+
// Remove event handler attributes dynamically
|
|
58
|
+
if (name.startsWith('on')) {
|
|
59
|
+
shouldRemove = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Remove if explicitly in dangerous attributes
|
|
63
|
+
if (!shouldRemove && dangerousAttributes.includes(name)) {
|
|
64
|
+
shouldRemove = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Remove javascript: URLs
|
|
68
|
+
if (!shouldRemove && value.toLowerCase().startsWith('javascript:')) {
|
|
69
|
+
shouldRemove = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Remove suspicious data: URLs (except for images in src)
|
|
73
|
+
if (!shouldRemove && value.toLowerCase().startsWith('data:') && name !== 'src') {
|
|
74
|
+
var dataType = value.substring(5).split(';')[0].split(',')[0];
|
|
75
|
+
if (!dataType.startsWith('image/')) {
|
|
76
|
+
shouldRemove = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Whitelist-based href handling
|
|
81
|
+
if (name === 'href') {
|
|
82
|
+
if (allowedHref.length === 0) {
|
|
83
|
+
shouldRemove = true;
|
|
84
|
+
} else {
|
|
85
|
+
var allowed = allowedHref.some(prefix => value.toLowerCase().startsWith(prefix));
|
|
86
|
+
if (!allowed) {
|
|
87
|
+
shouldRemove = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Whitelist-based src handling
|
|
93
|
+
if (name === 'src') {
|
|
94
|
+
var allowed = allowedSrc.some(prefix => value.toLowerCase().startsWith(prefix));
|
|
95
|
+
if (!allowed) {
|
|
96
|
+
shouldRemove = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Always remove form action
|
|
101
|
+
if (name === 'action') {
|
|
102
|
+
shouldRemove = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Override with preserve list
|
|
106
|
+
if (preserveAttributes.includes(name)) {
|
|
107
|
+
shouldRemove = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (shouldRemove) {
|
|
111
|
+
element.removeAttribute(attr.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Shadow DOM safety for form elements
|
|
116
|
+
if (options && options.createShadowRoot) {
|
|
117
|
+
if (['form', 'input', 'textarea', 'select', 'button'].includes(tagName)) {
|
|
118
|
+
if (!element.hasAttribute('data-shadow-form')) {
|
|
119
|
+
element.setAttribute('data-shadow-form', 'true');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Additional cleanup for shadow DOM: remove suspicious scripts from text content
|
|
126
|
+
if (options && options.createShadowRoot) {
|
|
127
|
+
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
|
|
128
|
+
var node;
|
|
129
|
+
while ((node = walker.nextNode())) {
|
|
130
|
+
var content = node.textContent;
|
|
131
|
+
if (content.toLowerCase().includes('<script') || content.toLowerCase().includes('javascript:')) {
|
|
132
|
+
node.textContent = content.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
133
|
+
.replace(/javascript:/gi, '');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof callback === 'function') {
|
|
139
|
+
callback(container);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
});
|
package/dist/test.html
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
(function (Q, $) {
|
|
2
|
+
/**
|
|
3
|
+
* @module Q-tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Implements lazy-loading for various types of elements.
|
|
8
|
+
* By default, has implementations for "img" and "Q_tool" selectors.
|
|
9
|
+
* The "Q_tool" handler only works with tools whose elements have a "data-q-lazyload" attribute .
|
|
10
|
+
* Note that the elements must have a stable, nonzero width height set in the CSS even
|
|
11
|
+
* if they are empty, otherwise they might not be lazy-loaded, and they might
|
|
12
|
+
* thrash back and forth if removed.
|
|
13
|
+
* @class Q lazyload
|
|
14
|
+
* @constructor
|
|
15
|
+
* @param {Object} [options] Override various options for this tool
|
|
16
|
+
* @param {Array} [options.handle] Set this to
|
|
17
|
+
* @param {Object} [options.handlers] You can modify the defaults by passing an object
|
|
18
|
+
* that looks like {handlerName: { selector: String, entering: Function, exiting: Function, preparing: Function}}
|
|
19
|
+
* Function "entering" receives (element, intersectionObserverEntry)
|
|
20
|
+
* Function "exiting" receives (element, intersectionObserverEntry)
|
|
21
|
+
* Function "preparing" receives (element) and is used the first time to prepare the element
|
|
22
|
+
* Both functions must return true if the element was modified.
|
|
23
|
+
* @param {Element} [options.root=tool.element] The container inside which to watch for intesections
|
|
24
|
+
* @param {Number} [options.waitUntilSlowerThan=1000] How many milliseconds to wait between entering events finally lazy-loading.
|
|
25
|
+
* Set to 0 if you want to load continuously while scrolling -- but this might cause jitter.
|
|
26
|
+
* @param {Object} [options.observerOptions] Override any options to pass to IntersectionObserver
|
|
27
|
+
* @param {Element} [options.observerOptions.root=tool.element.scrollingParent(true)]
|
|
28
|
+
* @param {String} [options.observerOptions.rootMargin='0px']
|
|
29
|
+
* @param {String} [options.observerOptions.threshold=0]
|
|
30
|
+
* @param {Boolean} [options.dontFreezeDimensions=false] Pass true to skip freezing dimensions when tools are removed
|
|
31
|
+
* @return {Q.Tool}
|
|
32
|
+
*/
|
|
33
|
+
Q.Tool.define('Q/lazyload', function (options) {
|
|
34
|
+
|
|
35
|
+
var tool = this;
|
|
36
|
+
var state = this.state;
|
|
37
|
+
state.root = state.root || this.element;
|
|
38
|
+
|
|
39
|
+
tool.onEnteringStopped = new Q.Event();
|
|
40
|
+
tool.frozenDimensions = new WeakMap();
|
|
41
|
+
|
|
42
|
+
var Elp = Element.prototype;
|
|
43
|
+
|
|
44
|
+
Q.ensure('IntersectionObserver', function () {
|
|
45
|
+
// Observe whatever is on the page already
|
|
46
|
+
var p = tool.element.scrollingParent(true);
|
|
47
|
+
if (p === document.body) {
|
|
48
|
+
p = document.documentElement;
|
|
49
|
+
}
|
|
50
|
+
tool.observer = _createObserver(tool, p);
|
|
51
|
+
tool.observe(tool.prepare(tool.element, false));
|
|
52
|
+
|
|
53
|
+
// Add mutation observer to detect direct DOM removals
|
|
54
|
+
Q.ensure('MutationObserver', function () {
|
|
55
|
+
var removalObserver = new MutationObserver(function (mutations) {
|
|
56
|
+
for (var mutation of mutations) {
|
|
57
|
+
for (var node of mutation.removedNodes) {
|
|
58
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
59
|
+
tool.unobserve([node]); // safety
|
|
60
|
+
forEachLazyElement(tool, node, function (element) {
|
|
61
|
+
for (var name in tool.state.handlers) {
|
|
62
|
+
var info = tool.state.handlers[name];
|
|
63
|
+
if (element.matches(info.selector)) {
|
|
64
|
+
if (element._Q_lazyload_exited
|
|
65
|
+
&& element._Q_lazyload_exited[name]
|
|
66
|
+
) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
element._Q_lazyload_exited = element._Q_lazyload_exited || {};
|
|
70
|
+
element._Q_lazyload_exited[name] = true;
|
|
71
|
+
info.exiting && info.exiting.call(tool, element);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
removalObserver.observe(state.root || document.body, {
|
|
79
|
+
childList: true,
|
|
80
|
+
subtree: true
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Override innerHTML
|
|
85
|
+
|
|
86
|
+
var originalSet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').set;
|
|
87
|
+
var originalGet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').get;
|
|
88
|
+
|
|
89
|
+
Object.defineProperty(Elp, 'innerHTML', {
|
|
90
|
+
set: function (html) {
|
|
91
|
+
var element = document.createElement('div');
|
|
92
|
+
var root = state.root || document.documentElement;
|
|
93
|
+
var inside = (root === this) || root.contains(this);
|
|
94
|
+
if (!inside) {
|
|
95
|
+
originalSet.call(this, html);
|
|
96
|
+
return html;
|
|
97
|
+
}
|
|
98
|
+
originalSet.call(element, html);
|
|
99
|
+
var found = false;
|
|
100
|
+
Q.each(state.handlers, function (name, info) {
|
|
101
|
+
var elements = element.querySelectorAll
|
|
102
|
+
? Array.from(element.querySelectorAll(info.selector))
|
|
103
|
+
: [];
|
|
104
|
+
if (elements.length) {
|
|
105
|
+
found = true;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
if (found) {
|
|
110
|
+
// prepare all images
|
|
111
|
+
tool.prepare(element, true);
|
|
112
|
+
}
|
|
113
|
+
originalSet.call(this, originalGet.call(element));
|
|
114
|
+
tool.observe(tool.prepare(this, true));
|
|
115
|
+
return html;
|
|
116
|
+
},
|
|
117
|
+
get: originalGet
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
Q.each(['insertBefore', 'appendChild', 'append', 'prepend'], function (i, fn) {
|
|
121
|
+
var orig = Elp[fn];
|
|
122
|
+
Elp[fn] = function smartInsert(element) {
|
|
123
|
+
if (element instanceof DocumentFragment) {
|
|
124
|
+
var children = Array.from(element.children);
|
|
125
|
+
for (var i=0, l=children.length; i<l; i++) {
|
|
126
|
+
smartInsert.apply(this, [children[i], arguments[1]]);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!(element instanceof HTMLElement)
|
|
131
|
+
|| Q.replace.lazyloadDontPrepare) {
|
|
132
|
+
return orig.apply(this, arguments);
|
|
133
|
+
}
|
|
134
|
+
var root = state.root || document.documentElement;
|
|
135
|
+
var inside = (root === this) || root.contains(this);
|
|
136
|
+
if (!inside) {
|
|
137
|
+
return orig.apply(this, arguments);
|
|
138
|
+
}
|
|
139
|
+
var found = false;
|
|
140
|
+
Q.each(state.handlers, function (name, info) {
|
|
141
|
+
if (element.matches && element.matches(info.selector)) {
|
|
142
|
+
found = true;
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
var elements = element.querySelectorAll
|
|
146
|
+
? Array.from(element.querySelectorAll(info.selector))
|
|
147
|
+
: [];
|
|
148
|
+
if (elements.length) {
|
|
149
|
+
found = true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (found) {
|
|
154
|
+
tool.observe(tool.prepare(element, true));
|
|
155
|
+
}
|
|
156
|
+
return orig.apply(this, arguments);
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
handlers: {
|
|
165
|
+
img: {
|
|
166
|
+
selector: 'img',
|
|
167
|
+
entering: function (img, entry) {
|
|
168
|
+
var tool = this;
|
|
169
|
+
if (Q.Visual.intersection(img, tool.state.root)) {
|
|
170
|
+
return _load();
|
|
171
|
+
}
|
|
172
|
+
tool.timeout && clearTimeout(tool.timeout);
|
|
173
|
+
tool.timeout = setTimeout(function () {
|
|
174
|
+
Q.handle(tool.onEnteringStopped, tool);
|
|
175
|
+
}, tool.state.waitUntilSlowerThan);
|
|
176
|
+
tool.onEnteringStopped.setOnce(_load);
|
|
177
|
+
return true;
|
|
178
|
+
|
|
179
|
+
function _load() {
|
|
180
|
+
var src = img.getAttribute('data-lazyload-src');
|
|
181
|
+
if (!src) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
img.setAttribute('src', Q.url(src));
|
|
185
|
+
img.removeAttribute('data-lazyload-src');
|
|
186
|
+
setTimeout(function () {
|
|
187
|
+
if (!_loadedImages[src]
|
|
188
|
+
&& !img.complete) {
|
|
189
|
+
img.addClass('Q_lazy_load');
|
|
190
|
+
}
|
|
191
|
+
_loadedImages[src] = true;
|
|
192
|
+
if (img.complete) {
|
|
193
|
+
_loaded();
|
|
194
|
+
}
|
|
195
|
+
img.addEventListener('load', _loaded);
|
|
196
|
+
}, 0);
|
|
197
|
+
|
|
198
|
+
function _loaded() {
|
|
199
|
+
img.addClass('Q_lazy_loaded');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
exiting: function (img) {
|
|
204
|
+
return true; // no need to do anything else
|
|
205
|
+
},
|
|
206
|
+
preparing: function (img, beingInsertedIntoDOM) {
|
|
207
|
+
if (!beingInsertedIntoDOM) {
|
|
208
|
+
return true; // too late anyway, browser will load image
|
|
209
|
+
}
|
|
210
|
+
if (img.hasClass('Q_lazy_load')
|
|
211
|
+
|| img.hasClass('Q_lazy_loaded')
|
|
212
|
+
|| img.hasClass('Q_no_lazyload')) {
|
|
213
|
+
return true; // this was already processed by lazy-loading
|
|
214
|
+
}
|
|
215
|
+
var src = img.getAttribute('src');
|
|
216
|
+
if (src && src.substring(0, 5) !== 'data:'
|
|
217
|
+
&& !img.hasAttribute('data-lazyload-src')) {
|
|
218
|
+
img.setAttribute('data-lazyload-src', Q.url(src));
|
|
219
|
+
img.setAttribute('src', Q.url(
|
|
220
|
+
Q.getObject('Q.images.lazyload.loadingSrc')
|
|
221
|
+
|| "{{Q}}/img/throbbers/transparent.gif"
|
|
222
|
+
));
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
tool: {
|
|
228
|
+
selector: '.Q_tool',
|
|
229
|
+
entering: function (element, entry) {
|
|
230
|
+
var tool = this;
|
|
231
|
+
if (Q.Visual.intersection(element, tool.state.root)) {
|
|
232
|
+
return _activate();
|
|
233
|
+
}
|
|
234
|
+
tool.timeout && clearTimeout(tool.timeout);
|
|
235
|
+
tool.timeout = setTimeout(function () {
|
|
236
|
+
Q.handle(tool.onEnteringStopped, tool);
|
|
237
|
+
}, tool.state.waitUntilSlowerThan);
|
|
238
|
+
tool.onEnteringStopped.setOnce(_activate);
|
|
239
|
+
return true;
|
|
240
|
+
|
|
241
|
+
function _activate() {
|
|
242
|
+
var ep = tool.frozenDimensions.get(element);
|
|
243
|
+
var c = element.parentElement;
|
|
244
|
+
var unfreeze = false;
|
|
245
|
+
if (!ep || !c) {
|
|
246
|
+
// element didn't exit before, so its dimensions weren't frozen
|
|
247
|
+
} else if (!tool.state.dontFreezeDimensions) {
|
|
248
|
+
var r = c.getBoundingClientRect();
|
|
249
|
+
if (!ep.containerRect || ep.containerRect.width !== r.width) {
|
|
250
|
+
// container width was resized, so throw away the frozen dimensions
|
|
251
|
+
// because a reflow should happen anyway
|
|
252
|
+
tool.frozenDimensions.delete(element);
|
|
253
|
+
element.removeClass('Q_frozen_dimensions');
|
|
254
|
+
} else {
|
|
255
|
+
// inform tools that their element has frozen dimensions,
|
|
256
|
+
// so the tools may want to revert the frozen dimensions
|
|
257
|
+
element.addClass('Q_frozen_dimensions');
|
|
258
|
+
element.setAttribute('data-Q-frozenDimensions', JSON.stringify(ep));
|
|
259
|
+
unfreeze = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (element.hasAttribute('data-q-lazyload')
|
|
263
|
+
&& (!element.Q || !element.Q.tool)) {
|
|
264
|
+
element.addClass('Q_lazy_load');
|
|
265
|
+
element.setAttribute('data-q-lazyload', 'activating');
|
|
266
|
+
Q.activate(element, {}, function () {
|
|
267
|
+
element.setAttribute('data-q-lazyload', 'activated');
|
|
268
|
+
element.addClass('Q_lazy_loaded');
|
|
269
|
+
if (unfreeze) {
|
|
270
|
+
tool.unfreezeDimensions(element);
|
|
271
|
+
}
|
|
272
|
+
}, {lazyload: true});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
exiting: function (element, entry) {
|
|
277
|
+
var tool = this;
|
|
278
|
+
if (element.hasAttribute('data-q-lazyload')
|
|
279
|
+
&& element.Q.tool) {
|
|
280
|
+
// Take a snapshot of the current width and height of the element,
|
|
281
|
+
// to restore it when it's later inserted back into the container,
|
|
282
|
+
// and prevent all the elements shifting. However, if the container
|
|
283
|
+
// itself is resizing, then we will remove this snapshot since things
|
|
284
|
+
// will shift anyway.
|
|
285
|
+
var cs = element.computedStyle();
|
|
286
|
+
tool.frozenDimensions.set(element, {
|
|
287
|
+
width: element.style.width,
|
|
288
|
+
height: element.style.height,
|
|
289
|
+
containerRect: element.parentElement.getBoundingClientRect()
|
|
290
|
+
});
|
|
291
|
+
if (!tool.state.dontFreezeDimensions) {
|
|
292
|
+
element.style.width = cs.width;
|
|
293
|
+
element.style.height = cs.height;
|
|
294
|
+
}
|
|
295
|
+
Q.Tool.remove(element);
|
|
296
|
+
element.removeClass('Q_lazy_loading');
|
|
297
|
+
element.removeClass('Q_lazy_loaded');
|
|
298
|
+
element.setAttribute('data-q-lazyload', 'removed');
|
|
299
|
+
if (this.state.handlers.tool.exitingRemoveHTML) {
|
|
300
|
+
element.innerHTML = '';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
},
|
|
305
|
+
preparing: function (element) {
|
|
306
|
+
return true;
|
|
307
|
+
},
|
|
308
|
+
exitingRemoveHTML: true
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
root: undefined,
|
|
312
|
+
observerOptions: {
|
|
313
|
+
root: undefined,
|
|
314
|
+
rootMargin: '0px',
|
|
315
|
+
threshold: 0
|
|
316
|
+
},
|
|
317
|
+
waitUntilSlowerThan: 1000,
|
|
318
|
+
dontFreezeDimensions: false
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
{
|
|
322
|
+
prepare: function (container, beingInsertedIntoDOM) {
|
|
323
|
+
var tool = this;
|
|
324
|
+
var found = [];
|
|
325
|
+
Q.each(tool.state.handlers, function (name, info) {
|
|
326
|
+
var elements = container.querySelectorAll
|
|
327
|
+
? Array.from(container.querySelectorAll(info.selector))
|
|
328
|
+
: [];
|
|
329
|
+
if (container.matches && container.matches(info.selector)) {
|
|
330
|
+
elements.push(container);
|
|
331
|
+
}
|
|
332
|
+
Q.each(elements, function (i, element) {
|
|
333
|
+
let ancestor = element.closest('[data-Q-retain]');
|
|
334
|
+
if (ancestor && Q.replace.retainedElements[ancestor.id]) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (info.preparing.call(tool, element, beingInsertedIntoDOM) === true) {
|
|
338
|
+
found.push(element);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
return found;
|
|
343
|
+
},
|
|
344
|
+
observe: function (elements) {
|
|
345
|
+
var tool = this;
|
|
346
|
+
tool.observer && Q.each(elements, function (i, element) {
|
|
347
|
+
tool.observer.observe(element);
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
unobserve: function (elements) {
|
|
351
|
+
var tool = this;
|
|
352
|
+
tool.observer && Q.each(elements, function (i, element) {
|
|
353
|
+
tool.observer.unobserve(element);
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
unfreezeDimensions: function(element) {
|
|
357
|
+
var ep = this.frozenDimensions.get(element);
|
|
358
|
+
if (!ep) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (ep.width) {
|
|
362
|
+
element.style.width = ep.width;
|
|
363
|
+
} else {
|
|
364
|
+
element.style.removeProperty('width');
|
|
365
|
+
}
|
|
366
|
+
if (ep.height) {
|
|
367
|
+
element.style.height = ep.height;
|
|
368
|
+
} else {
|
|
369
|
+
element.style.removeProperty('height');
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
Q: {
|
|
373
|
+
beforeRemove: function () {
|
|
374
|
+
this.observer && this.observer.disconnect();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
function _createObserver(tool, container) {
|
|
380
|
+
var o = Q.copy(tool.state.observerOptions);
|
|
381
|
+
if (o.root === undefined) {
|
|
382
|
+
o.root = (container === document.documentElement) ? null : (container || null);
|
|
383
|
+
}
|
|
384
|
+
return new IntersectionObserver(function (entries, observer) {
|
|
385
|
+
Q.each(entries, function (i, entry) {
|
|
386
|
+
Q.each(tool.state.handlers, function (name, info) {
|
|
387
|
+
if (Q.replace.retainedElements[entry.target.id]) {
|
|
388
|
+
// next time around, we'll process this element
|
|
389
|
+
delete Q.replace.retainedElements[entry.target.id];
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (entry.target.matches && entry.target.matches(info.selector)) {
|
|
393
|
+
if (entry.isIntersecting) {
|
|
394
|
+
if (entry.target._Q_lazyload_exited) {
|
|
395
|
+
delete entry.target._Q_lazyload_exited[name];
|
|
396
|
+
}
|
|
397
|
+
info.entering.call(tool, entry.target, entry);
|
|
398
|
+
} else {
|
|
399
|
+
var rect = entry.target.getBoundingClientRect();
|
|
400
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
401
|
+
// also covers !document.body.contains(entry.target)
|
|
402
|
+
info.exiting.call(tool, entry.target, entry);
|
|
403
|
+
} // otherwise it might get a false positive
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}, o);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
Q.lazyload = Q.lazyload || {};
|
|
412
|
+
var _loadedImages = Q.lazyload.loadedImages = {};
|
|
413
|
+
|
|
414
|
+
function forEachLazyElement(tool, container, callback) {
|
|
415
|
+
for (var name in tool.state.handlers || {}) {
|
|
416
|
+
var handler = tool.state.handlers[name];
|
|
417
|
+
if (!handler || !handler.selector) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (!(container instanceof HTMLElement)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
var elements = container.querySelectorAll(handler.selector) || [];
|
|
424
|
+
if (container.matches(handler.selector)) {
|
|
425
|
+
callback(container);
|
|
426
|
+
}
|
|
427
|
+
for (var i=0; i<elements.length; ++i) {
|
|
428
|
+
callback(elements[i]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
})(Q, Q.jQuery);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qbix/q",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Q.js, the Qbix JavaScript framework",
|
|
5
|
+
"main": "dist/Q.min.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/"
|
|
8
|
+
],
|
|
9
|
+
"author": "Qbix Inc.",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node test/metrics.test.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Qbix/Q.js.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/Qbix/Q.js/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/Qbix/Q.js#readme",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"jsdom": "^30.0.1"
|
|
25
|
+
}
|
|
26
|
+
}
|