├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── README.md ├── dist ├── jquery.fracs.js └── jquery.fracs.min.js ├── ghu.js ├── package-lock.json ├── package.json └── src ├── demo ├── index.html.pug ├── main.js └── main.less ├── jquery.fracs.js └── test ├── index.html.pug ├── main.js └── main.less /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | 15 | [{*.json,*.yml}] 16 | indent_size = 2 17 | 18 | 19 | [{*.md,*.pug}] 20 | trim_trailing_whitespace = false 21 | 22 | 23 | [*.svg] 24 | insert_final_newline = false 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /coverage/ 3 | /dist/ 4 | /vendor/ 5 | /node_modules/ 6 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | 4 | env: 5 | es6: true 6 | node: true 7 | 8 | rules: 9 | array-bracket-spacing: [2, never] 10 | arrow-parens: [2, as-needed] 11 | arrow-spacing: 2 12 | block-scoped-var: 2 13 | brace-style: [2, 1tbs, {allowSingleLine: true}] 14 | camelcase: 0 15 | comma-dangle: [2, never] 16 | comma-spacing: [2, {before: false, after: true}] 17 | comma-style: [2, last] 18 | complexity: [1, 16] ### 19 | computed-property-spacing: [2, never] 20 | consistent-return: 2 21 | consistent-this: [2, self] 22 | constructor-super: 2 23 | curly: [2, multi-line] 24 | default-case: 2 25 | dot-location: [2, property] 26 | dot-notation: [2, {allowKeywords: true}] 27 | eol-last: 2 28 | eqeqeq: 2 29 | func-names: 2 30 | func-style: [2, declaration, {allowArrowFunctions: true}] 31 | generator-star-spacing: [2, after] 32 | guard-for-in: 2 33 | handle-callback-err: 2 34 | indent: [2, 4] 35 | key-spacing: [2, {beforeColon: false, afterColon: true}] 36 | keyword-spacing: [2, {before: true, after: true}] 37 | linebreak-style: [2, unix] 38 | max-depth: [1, 4] 39 | max-len: [0, 80, 4] 40 | max-nested-callbacks: [1, 3] 41 | max-params: [1, 8] ### 42 | max-statements: [1, 48] ### 43 | new-cap: 0 44 | new-parens: 2 45 | newline-after-var: 0 46 | no-alert: 2 47 | no-array-constructor: 2 48 | no-bitwise: 2 49 | no-caller: 2 50 | no-catch-shadow: 2 51 | no-class-assign: 2 52 | no-cond-assign: 2 53 | no-console: 0 ### 54 | no-const-assign: 2 55 | no-constant-condition: 1 56 | no-continue: 0 57 | no-control-regex: 2 58 | no-debugger: 2 59 | no-delete-var: 2 60 | no-div-regex: 2 61 | no-dupe-args: 2 62 | no-dupe-class-members: 2 63 | no-dupe-keys: 2 64 | no-duplicate-case: 2 65 | no-else-return: 1 66 | no-empty: 2 67 | no-empty-character-class: 2 68 | no-empty-pattern: 2 69 | no-eq-null: 2 70 | no-eval: 2 71 | no-ex-assign: 2 72 | no-extend-native: 1 73 | no-extra-bind: 2 74 | no-extra-boolean-cast: 2 75 | no-extra-parens: 1 76 | no-extra-semi: 2 77 | no-fallthrough: 2 78 | no-floating-decimal: 2 79 | no-func-assign: 2 80 | no-implicit-coercion: [2, {boolean: false, number: true, string: true}] 81 | no-implied-eval: 2 82 | no-inline-comments: 0 83 | no-inner-declarations: [2, functions] 84 | no-invalid-regexp: 2 85 | no-invalid-this: 2 86 | no-irregular-whitespace: 2 87 | no-iterator: 2 88 | no-label-var: 2 89 | no-labels: 2 90 | no-lone-blocks: 2 91 | no-lonely-if: 2 92 | no-loop-func: 1 93 | no-magic-numbers: 0 94 | no-mixed-requires: [2, false] 95 | no-mixed-spaces-and-tabs: [2, false] 96 | no-multi-spaces: 2 97 | no-multi-str: 2 98 | no-multiple-empty-lines: [2, {max: 4}] 99 | no-native-reassign: 1 100 | no-negated-in-lhs: 2 101 | no-nested-ternary: 0 102 | no-new: 2 103 | no-new-func: 2 104 | no-new-object: 2 105 | no-new-require: 2 106 | no-new-wrappers: 2 107 | no-obj-calls: 2 108 | no-octal: 2 109 | no-octal-escape: 2 110 | no-param-reassign: 0 111 | no-path-concat: 2 112 | no-plusplus: 2 113 | no-process-env: 2 114 | no-process-exit: 2 115 | no-proto: 2 116 | no-redeclare: 2 117 | no-regex-spaces: 2 118 | no-restricted-modules: 2 119 | no-return-assign: 2 120 | no-script-url: 2 121 | no-self-compare: 2 122 | no-sequences: 2 123 | no-shadow: 2 124 | no-shadow-restricted-names: 2 125 | no-spaced-func: 2 126 | no-sparse-arrays: 2 127 | no-sync: 0 128 | no-ternary: 0 129 | no-this-before-super: 2 130 | no-throw-literal: 1 131 | no-trailing-spaces: 2 132 | no-undef: 2 133 | no-undef-init: 2 134 | no-undefined: 0 135 | no-underscore-dangle: 0 136 | no-unexpected-multiline: 2 137 | no-unneeded-ternary: 2 138 | no-unreachable: 2 139 | no-useless-call: 2 140 | no-useless-concat: 2 141 | no-unused-expressions: 2 142 | no-unused-vars: [1, {vars: all, args: after-used}] 143 | no-use-before-define: 2 144 | no-var: 2 145 | no-void: 2 146 | no-warning-comments: [1, {terms: [todo, fixme, xxx], location: start}] 147 | no-with: 2 148 | object-curly-spacing: [2, never] 149 | object-shorthand: [2, always] 150 | one-var: [2, never] 151 | operator-assignment: [2, always] 152 | operator-linebreak: [2, after] 153 | padded-blocks: [2, never] 154 | prefer-arrow-callback: 2 155 | prefer-const: 1 156 | prefer-reflect: 1 157 | prefer-spread: 2 158 | prefer-template: 0 ### 159 | quote-props: [2, as-needed] 160 | quotes: [2, single, avoid-escape] 161 | radix: 2 162 | require-yield: 2 163 | semi: 2 164 | semi-spacing: [2, {before: false, after: true}] 165 | sort-vars: 0 166 | space-before-blocks: [2, always] 167 | space-before-function-paren: [2, {anonymous: always, named: never}] 168 | space-in-parens: [2, never] 169 | space-infix-ops: 2 170 | space-unary-ops: [2, {words: true, nonwords: false}] 171 | spaced-comment: [2, always] 172 | strict: [2, never] 173 | use-isnan: 2 174 | valid-jsdoc: 2 175 | valid-typeof: 2 176 | vars-on-top: 0 177 | wrap-iife: [2, outside] 178 | wrap-regex: 2 179 | yoda: [2, never, {exceptRange: true}] 180 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /coverage/ 3 | /local/ 4 | /node_modules/ 5 | /npm-debug.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery.fracs 2 | 3 | [![license][license-img]][github] [![web][web-img]][web] [![github][github-img]][github] 4 | 5 | jQuery plugin to determine the visible fractions of HTML elements. 6 | 7 | 8 | ## License 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2020 Lars Jung (https://larsjung.de) 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | 32 | [web]: https://larsjung.de/jquery-fracs/ 33 | [github]: https://github.com/lrsjng/jquery-fracs 34 | 35 | [license-img]: https://img.shields.io/badge/license-MIT-a0a060.svg?style=flat-square 36 | [web-img]: https://img.shields.io/badge/web-larsjung.de/jquery--fracs-a0a060.svg?style=flat-square 37 | [github-img]: https://img.shields.io/badge/github-lrsjng/jquery--fracs-a0a060.svg?style=flat-square 38 | -------------------------------------------------------------------------------- /dist/jquery.fracs.js: -------------------------------------------------------------------------------- 1 | /*! jquery-fracs v1.0.2 - https://larsjung.de/jquery-fracs/ */ 2 | "use strict"; 3 | 4 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 5 | 6 | (function () { 7 | var WIN = window; // eslint-disable-line 8 | 9 | var DOC = WIN.document; 10 | var $ = WIN.jQuery; 11 | var $WIN = $(WIN); 12 | var $DOC = $(DOC); 13 | var extend = $.extend; 14 | var is_fn = $.isFunction; 15 | var math_max = Math.max; 16 | var math_min = Math.min; 17 | var math_round = Math.round; 18 | 19 | var is_typeof = function is_typeof(obj, type) { 20 | return _typeof(obj) === type; 21 | }; 22 | 23 | var is_instanceof = function is_instanceof(obj, type) { 24 | return obj instanceof type; 25 | }; 26 | 27 | var is_html_el = function is_html_el(obj) { 28 | return obj && obj.nodeType; 29 | }; 30 | 31 | var get_html_el = function get_html_el(obj) { 32 | return is_html_el(obj) ? obj : is_instanceof(obj, $) ? obj[0] : undefined; 33 | }; 34 | 35 | var get_id = function () { 36 | var ids = {}; 37 | var next_id = 1; 38 | return function (el) { 39 | if (!el) { 40 | return 0; 41 | } 42 | 43 | if (!ids[el]) { 44 | ids[el] = next_id; 45 | next_id += 1; 46 | } 47 | 48 | return ids[el]; 49 | }; 50 | }(); 51 | 52 | var equal = function equal(x, y, props) { 53 | if (x === y) { 54 | return true; 55 | } 56 | 57 | if (!x || !y || x.constructor !== y.constructor) { 58 | return false; 59 | } 60 | 61 | for (var i = 0, l = props.length; i < l; i += 1) { 62 | var prop = props[i]; 63 | 64 | if (x[prop] && is_fn(x[prop].equals) && !x[prop].equals(y[prop])) { 65 | return false; 66 | } 67 | 68 | if (x[prop] !== y[prop]) { 69 | return false; 70 | } 71 | } 72 | 73 | return true; 74 | }; 75 | 76 | function Rect(left, top, width, height) { 77 | this.left = math_round(left); 78 | this.top = math_round(top); 79 | this.width = math_round(width); 80 | this.height = math_round(height); 81 | this.right = this.left + this.width; 82 | this.bottom = this.top + this.height; 83 | } 84 | 85 | extend(Rect.prototype, { 86 | equals: function equals(that) { 87 | return equal(this, that, ['left', 'top', 'width', 'height']); 88 | }, 89 | area: function area() { 90 | return this.width * this.height; 91 | }, 92 | relativeTo: function relativeTo(rect) { 93 | return new Rect(this.left - rect.left, this.top - rect.top, this.width, this.height); 94 | }, 95 | intersection: function intersection(rect) { 96 | if (!is_instanceof(rect, Rect)) { 97 | return null; 98 | } 99 | 100 | var left = math_max(this.left, rect.left); 101 | var right = math_min(this.right, rect.right); 102 | var top = math_max(this.top, rect.top); 103 | var bottom = math_min(this.bottom, rect.bottom); 104 | var width = right - left; 105 | var height = bottom - top; 106 | return width >= 0 && height >= 0 ? new Rect(left, top, width, height) : null; 107 | }, 108 | envelope: function envelope(rect) { 109 | if (!is_instanceof(rect, Rect)) { 110 | return this; 111 | } 112 | 113 | var left = math_min(this.left, rect.left); 114 | var right = math_max(this.right, rect.right); 115 | var top = math_min(this.top, rect.top); 116 | var bottom = math_max(this.bottom, rect.bottom); 117 | var width = right - left; 118 | var height = bottom - top; 119 | return new Rect(left, top, width, height); 120 | } 121 | }); 122 | extend(Rect, { 123 | ofContent: function ofContent(el, in_content_space) { 124 | if (!el || el === DOC || el === WIN) { 125 | return new Rect(0, 0, $DOC.width(), $DOC.height()); 126 | } 127 | 128 | if (in_content_space) { 129 | return new Rect(0, 0, el.scrollWidth, el.scrollHeight); 130 | } 131 | 132 | return new Rect(el.offsetLeft - el.scrollLeft, el.offsetTop - el.scrollTop, el.scrollWidth, el.scrollHeight); 133 | }, 134 | ofViewport: function ofViewport(el, in_content_space) { 135 | if (!el || el === DOC || el === WIN) { 136 | return new Rect($WIN.scrollLeft(), $WIN.scrollTop(), $WIN.width(), $WIN.height()); 137 | } 138 | 139 | if (in_content_space) { 140 | return new Rect(el.scrollLeft, el.scrollTop, el.clientWidth, el.clientHeight); 141 | } 142 | 143 | return new Rect(el.offsetLeft, el.offsetTop, el.clientWidth, el.clientHeight); 144 | }, 145 | ofElement: function ofElement(el) { 146 | var $el = $(el); 147 | 148 | if (!$el.is(':visible')) { 149 | return null; 150 | } 151 | 152 | var offset = $el.offset(); 153 | return new Rect(offset.left, offset.top, $el.outerWidth(), $el.outerHeight()); 154 | } 155 | }); 156 | 157 | function Fractions(visible, viewport, possible, rects) { 158 | this.visible = visible || 0; 159 | this.viewport = viewport || 0; 160 | this.possible = possible || 0; 161 | this.rects = rects && extend({}, rects) || null; 162 | } 163 | 164 | extend(Fractions.prototype, { 165 | equals: function equals(that) { 166 | return this.fracsEqual(that) && this.rectsEqual(that); 167 | }, 168 | fracsEqual: function fracsEqual(that) { 169 | return equal(this, that, ['visible', 'viewport', 'possible']); 170 | }, 171 | rectsEqual: function rectsEqual(that) { 172 | return equal(this.rects, that.rects, ['document', 'element', 'viewport']); 173 | } 174 | }); 175 | extend(Fractions, { 176 | of: function of(rect, viewport) { 177 | rect = is_html_el(rect) && Rect.ofElement(rect) || rect; 178 | viewport = is_html_el(viewport) && Rect.ofViewport(viewport) || viewport || Rect.ofViewport(); 179 | 180 | if (!is_instanceof(rect, Rect)) { 181 | return new Fractions(); 182 | } 183 | 184 | var intersection = rect.intersection(viewport); 185 | 186 | if (!intersection) { 187 | return new Fractions(); 188 | } 189 | 190 | var intersection_area = intersection.area(); 191 | var possible_area = math_min(rect.width, viewport.width) * math_min(rect.height, viewport.height); 192 | return new Fractions(intersection_area / rect.area(), intersection_area / viewport.area(), intersection_area / possible_area, { 193 | document: intersection, 194 | element: intersection.relativeTo(rect), 195 | viewport: intersection.relativeTo(viewport) 196 | }); 197 | } 198 | }); 199 | 200 | function Group(els, viewport) { 201 | this.els = els; 202 | this.viewport = viewport; 203 | } 204 | 205 | var RECT_PROPS = ['width', 'height', 'left', 'right', 'top', 'bottom']; 206 | var FRACS_PROPS = ['possible', 'visible', 'viewport']; 207 | 208 | var get_value = function get_value(el, viewport, prop) { 209 | var obj; 210 | 211 | if (RECT_PROPS.includes(prop)) { 212 | obj = Rect.ofElement(el); 213 | } else if (FRACS_PROPS.includes(prop)) { 214 | obj = Fractions.of(el, viewport); 215 | } 216 | 217 | return obj ? obj[prop] : 0; 218 | }; 219 | 220 | var sort_asc = function sort_asc(x, y) { 221 | return x.val - y.val; 222 | }; 223 | 224 | var sort_desc = function sort_desc(x, y) { 225 | return y.val - x.val; 226 | }; 227 | 228 | extend(Group.prototype, { 229 | sorted: function sorted(prop, desc) { 230 | var viewport = this.viewport; 231 | return $.map(this.els, function (el) { 232 | return { 233 | el: el, 234 | val: get_value(el, viewport, prop) 235 | }; 236 | }).sort(desc ? sort_desc : sort_asc); 237 | }, 238 | best: function best(prop, desc) { 239 | return this.els.length ? this.sorted(prop, desc)[0] : null; 240 | } 241 | }); 242 | 243 | function ScrollState(el) { 244 | var content = Rect.ofContent(el, true); 245 | var viewport = Rect.ofViewport(el, true); 246 | var w = content.width - viewport.width; 247 | var h = content.height - viewport.height; 248 | this.content = content; 249 | this.viewport = viewport; 250 | this.width = w <= 0 ? null : viewport.left / w; 251 | this.height = h <= 0 ? null : viewport.top / h; 252 | this.left = viewport.left; 253 | this.top = viewport.top; 254 | this.right = content.right - viewport.right; 255 | this.bottom = content.bottom - viewport.bottom; 256 | } 257 | 258 | extend(ScrollState.prototype, { 259 | equals: function equals(that) { 260 | return equal(this, that, ['width', 'height', 'left', 'top', 'right', 'bottom', 'content', 'viewport']); 261 | } 262 | }); 263 | 264 | function Viewport(el) { 265 | this.el = el || WIN; 266 | } 267 | 268 | extend(Viewport.prototype, { 269 | equals: function equals(that) { 270 | return equal(this, that, ['el']); 271 | }, 272 | scrollState: function scrollState() { 273 | return new ScrollState(this.el); 274 | }, 275 | scrollTo: function scrollTo(left, top, duration) { 276 | var $el = this.el === WIN ? $('html,body') : $(this.el); 277 | left = left || 0; 278 | top = top || 0; 279 | duration = isNaN(duration) ? 1000 : duration; 280 | $el.stop(true).animate({ 281 | scrollLeft: left, 282 | scrollTop: top 283 | }, duration); 284 | }, 285 | scrollToRect: function scrollToRect(rect, left, top, duration) { 286 | left = left || 0; 287 | top = top || 0; 288 | this.scrollTo(rect.left - left, rect.top - top, duration); 289 | }, 290 | scrollToElement: function scrollToElement(el, left, top, duration) { 291 | var rect = Rect.ofElement(el).relativeTo(Rect.ofContent(this.el)); 292 | this.scrollToRect(rect, left, top, duration); 293 | } 294 | }); 295 | var callback_mixin = { 296 | context: null, 297 | updatedValue: function updatedValue() { 298 | return null; 299 | }, 300 | init: function init(target) { 301 | this.callbacks = $.Callbacks('memory unique'); 302 | this.curr_val = null; 303 | this.prev_val = null; 304 | $(target || WIN).on('load resize scroll', $.proxy(this.check, this)); 305 | }, 306 | bind: function bind(callback) { 307 | this.callbacks.add(callback); 308 | }, 309 | unbind: function unbind(callback) { 310 | if (callback) { 311 | this.callbacks.remove(callback); 312 | } else { 313 | this.callbacks.empty(); 314 | } 315 | }, 316 | check: function check(event) { 317 | var val = this.updatedValue(event); 318 | 319 | if (val === undefined) { 320 | return false; 321 | } 322 | 323 | this.prev_val = this.curr_val; 324 | this.curr_val = val; 325 | this.callbacks.fireWith(this.context, [this.curr_val, this.prev_val]); 326 | return true; 327 | } 328 | }; 329 | 330 | function FracsCallbacks(el, viewport) { 331 | this.context = el; 332 | this.viewport = viewport; 333 | this.init(); 334 | } 335 | 336 | extend(FracsCallbacks.prototype, callback_mixin, { 337 | updatedValue: function updatedValue() { 338 | var val = Fractions.of(this.context, this.viewport); 339 | 340 | if (!val.equals(this.curr_val)) { 341 | return val; 342 | } 343 | 344 | return undefined; 345 | } 346 | }); 347 | 348 | function GroupCallbacks(els, viewport, prop, desc) { 349 | this.context = new Group(els, viewport); 350 | this.property = prop; 351 | this.descending = desc; 352 | this.init(); 353 | } 354 | 355 | extend(GroupCallbacks.prototype, callback_mixin, { 356 | updatedValue: function updatedValue() { 357 | var best = this.context.best(this.property, this.descending); 358 | 359 | if (best) { 360 | best = best.val > 0 ? best.el : null; 361 | 362 | if (this.curr_val !== best) { 363 | return best; 364 | } 365 | } 366 | 367 | return undefined; 368 | } 369 | }); 370 | 371 | function ScrollStateCallbacks(el) { 372 | if (!el || el === WIN || el === DOC) { 373 | this.context = WIN; 374 | } else { 375 | this.context = el; 376 | } 377 | 378 | this.init(this.context); 379 | } 380 | 381 | extend(ScrollStateCallbacks.prototype, callback_mixin, { 382 | updatedValue: function updatedValue() { 383 | var val = new ScrollState(this.context); 384 | 385 | if (!val.equals(this.curr_val)) { 386 | return val; 387 | } 388 | 389 | return undefined; 390 | } 391 | }); // # Public API 392 | // accessible via `$(selector).fracs('', ...)`. 393 | 394 | var methods = { 395 | // ## 'content' 396 | // Returns the content rect of the first selected element in content space. 397 | // If no element is selected it returns the document rect. 398 | content: function content(in_content_space) { 399 | return this.length ? Rect.ofContent(this[0], in_content_space) : null; 400 | }, 401 | // ## 'envelope' 402 | // Returns the smallest rectangle that containes all selected elements. 403 | envelope: function envelope() { 404 | var res; 405 | $.each(this, function (idx, el) { 406 | var rect = Rect.ofElement(el); 407 | res = res ? res.envelope(rect) : rect; 408 | }); 409 | return res; 410 | }, 411 | // ## 'fracs' 412 | // This is the **default method**. So the first parameter `'fracs'` 413 | // can be omitted. 414 | // 415 | // Returns the fractions for the first selected element. 416 | // 417 | // .fracs(): Fractions 418 | // 419 | // Binds a callback function that will be invoked if fractions have changed 420 | // after a `window resize` or `window scroll` event. 421 | // 422 | // .fracs(callback(fracs: Fractions, prev_fracs: Fractions)): jQuery 423 | // 424 | // Unbinds the specified callback function. 425 | // 426 | // .fracs('unbind', callback): jQuery 427 | // 428 | // Unbinds all callback functions. 429 | // 430 | // .fracs('unbind'): jQuery 431 | // 432 | // Checks if fractions changed and if so invokes all bound callback functions. 433 | // 434 | // .fracs('check'): jQuery 435 | fracs: function fracs(action, callback, viewport) { 436 | if (!is_typeof(action, 'string')) { 437 | viewport = callback; 438 | callback = action; 439 | action = null; 440 | } 441 | 442 | if (!is_fn(callback)) { 443 | viewport = callback; 444 | callback = null; 445 | } 446 | 447 | viewport = get_html_el(viewport); 448 | var ns = 'fracs.' + get_id(viewport); 449 | 450 | if (action === 'unbind') { 451 | return this.each(function cb() { 452 | var cbs = $(this).data(ns); 453 | 454 | if (cbs) { 455 | cbs.unbind(callback); 456 | } 457 | }); 458 | } else if (action === 'check') { 459 | return this.each(function cb() { 460 | var cbs = $(this).data(ns); 461 | 462 | if (cbs) { 463 | cbs.check(); 464 | } 465 | }); 466 | } else if (is_fn(callback)) { 467 | return this.each(function cb() { 468 | var $this = $(this); 469 | var cbs = $this.data(ns); 470 | 471 | if (!cbs) { 472 | cbs = new FracsCallbacks(this, viewport); 473 | $this.data(ns, cbs); 474 | } 475 | 476 | cbs.bind(callback); 477 | }); 478 | } 479 | 480 | return this.length ? Fractions.of(this[0], viewport) : null; 481 | }, 482 | // ## 'intersection' 483 | // Returns the greatest rectangle that is contained in all selected elements. 484 | intersection: function intersection() { 485 | var res; 486 | $.each(this, function (idx, el) { 487 | var rect = Rect.ofElement(el); 488 | res = res ? res.intersection(rect) : rect; 489 | }); 490 | return res; 491 | }, 492 | // ## 'max' 493 | // Reduces the set of selected elements to those with the maximum value 494 | // of the specified property. 495 | // Valid values for property are `possible`, `visible`, `viewport`, 496 | // `width`, `height`, `left`, `right`, `top`, `bottom`. 497 | // 498 | // .fracs('max', property: String): jQuery 499 | // 500 | // Binds a callback function to the set of selected elements that gets 501 | // triggert whenever the element with the highest value of the specified 502 | // property changes. 503 | // 504 | // .fracs('max', property: String, callback(best: Element, prev_best: Element)): jQuery 505 | max: function max(prop, callback, viewport) { 506 | if (!is_fn(callback)) { 507 | viewport = callback; 508 | callback = null; 509 | } 510 | 511 | viewport = get_html_el(viewport); 512 | 513 | if (callback) { 514 | new GroupCallbacks(this, viewport, prop, true).bind(callback); 515 | return this; 516 | } 517 | 518 | return this.pushStack(new Group(this, viewport).best(prop, true).el); 519 | }, 520 | // ## 'min' 521 | // Reduces the set of selected elements to those with the minimum value 522 | // of the specified property. 523 | // Valid values for property are `possible`, `visible`, `viewport`, 524 | // `width`, `height`, `left`, `right`, `top`, `bottom`. 525 | // 526 | // .fracs('min', property: String): jQuery 527 | // 528 | // Binds a callback function to the set of selected elements that gets 529 | // triggert whenever the element with the lowest value of the specified 530 | // property changes. 531 | // 532 | // .fracs('min', property: String, callback(best: Element, prev_best: Element)): jQuery 533 | min: function min(prop, callback, viewport) { 534 | if (!is_fn(callback)) { 535 | viewport = callback; 536 | callback = null; 537 | } 538 | 539 | viewport = get_html_el(viewport); 540 | 541 | if (callback) { 542 | new GroupCallbacks(this, viewport, prop).bind(callback); 543 | return this; 544 | } 545 | 546 | return this.pushStack(new Group(this, viewport).best(prop).el); 547 | }, 548 | // ## 'rect' 549 | // Returns the dimensions for the first selected element in document space. 550 | rect: function rect() { 551 | return this.length ? Rect.ofElement(this[0]) : null; 552 | }, 553 | // ## 'scrollState' 554 | // Returns the current scroll state for the first selected element. 555 | // 556 | // .fracs('scrollState'): ScrollState 557 | // 558 | // Binds a callback function that will be invoked if scroll state has changed 559 | // after a `resize` or `scroll` event. 560 | // 561 | // .fracs('scrollState', callback(scrollState: scrollState, prevScrollState: scrollState)): jQuery 562 | // 563 | // Unbinds the specified callback function. 564 | // 565 | // .fracs('scrollState', 'unbind', callback): jQuery 566 | // 567 | // Unbinds all callback functions. 568 | // 569 | // .fracs('scrollState', 'unbind'): jQuery 570 | // 571 | // Checks if scroll state changed and if so invokes all bound callback functions. 572 | // 573 | // .fracs('scrollState', 'check'): jQuery 574 | scrollState: function scrollState(action, callback) { 575 | var ns = 'fracs.scrollState'; 576 | 577 | if (!is_typeof(action, 'string')) { 578 | callback = action; 579 | action = null; 580 | } 581 | 582 | if (action === 'unbind') { 583 | return this.each(function cb() { 584 | var cbs = $(this).data(ns); 585 | 586 | if (cbs) { 587 | cbs.unbind(callback); 588 | } 589 | }); 590 | } else if (action === 'check') { 591 | return this.each(function cb() { 592 | var cbs = $(this).data(ns); 593 | 594 | if (cbs) { 595 | cbs.check(); 596 | } 597 | }); 598 | } else if (is_fn(callback)) { 599 | return this.each(function cb() { 600 | var $this = $(this); 601 | var cbs = $this.data(ns); 602 | 603 | if (!cbs) { 604 | cbs = new ScrollStateCallbacks(this); 605 | $this.data(ns, cbs); 606 | } 607 | 608 | cbs.bind(callback); 609 | }); 610 | } 611 | 612 | return this.length ? new ScrollState(this[0]) : null; 613 | }, 614 | // ## 'scroll' 615 | // Scrolls the selected elements relative to its current position, 616 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 617 | // 618 | // .fracs('scroll', element: HTMLElement/jQuery, [left: int,] [top: int,] [duration: int]): jQuery 619 | scroll: function scroll(left, top, duration) { 620 | return this.each(function cb() { 621 | new Viewport(this).scroll(left, top, duration); 622 | }); 623 | }, 624 | // ## 'scrollTo' 625 | // Scrolls the selected elements to the specified element or an absolute position, 626 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 627 | // 628 | // .fracs('scrollTo', element: HTMLElement/jQuery, [left: int,] [top: int,] [duration: int]): jQuery 629 | // .fracs('scrollTo', [left: int,] [top: int,] [duration: int]): jQuery 630 | scrollTo: function scrollTo(el, left, top, duration) { 631 | if ($.isNumeric(el)) { 632 | duration = top; 633 | top = left; 634 | left = el; 635 | el = null; 636 | } 637 | 638 | el = get_html_el(el); 639 | return this.each(function cb() { 640 | if (el) { 641 | new Viewport(this).scrollToElement(el, left, top, duration); 642 | } else { 643 | new Viewport(this).scrollTo(left, top, duration); 644 | } 645 | }); 646 | }, 647 | // ## 'scrollToThis' 648 | // Scrolls the viewport (defaults to window) to the first selected element in the specified time, 649 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 650 | scrollToThis: function scrollToThis(left, top, duration, viewport) { 651 | viewport = new Viewport(get_html_el(viewport)); 652 | viewport.scrollToElement(this[0], left, top, duration); 653 | return this; 654 | }, 655 | // ## 'sort' 656 | // Sorts the set of selected elements by the specified prop. 657 | // Valid values for prop are `possible`, `visible`, `viewport`, 658 | // `width`, `height`, `left`, `right`, `top`, `bottom`. The default 659 | // sort order is descending. 660 | sort: function sort(prop, ascending, viewport) { 661 | if (!is_typeof(ascending, 'boolean')) { 662 | viewport = ascending; 663 | ascending = null; 664 | } 665 | 666 | viewport = get_html_el(viewport); 667 | return this.pushStack($.map(new Group(this, viewport).sorted(prop, !ascending), function (entry) { 668 | return entry.el; 669 | })); 670 | }, 671 | // ## 'viewport' 672 | // Returns the current viewport of the first selected element. 673 | // If no element is selected it returns the document's viewport. 674 | viewport: function viewport(in_content_space) { 675 | return this.length ? Rect.ofViewport(this[0], in_content_space) : null; 676 | } 677 | }; 678 | 679 | $.fracs = function (rect, viewport) { 680 | return Fractions.of(rect, viewport); 681 | }; 682 | 683 | $.fracs._ = { 684 | // published for testing 685 | Rect: Rect, 686 | Fractions: Fractions, 687 | Group: Group, 688 | ScrollState: ScrollState, 689 | Viewport: Viewport, 690 | FracsCallbacks: FracsCallbacks, 691 | GroupCallbacks: GroupCallbacks, 692 | ScrollStateCallbacks: ScrollStateCallbacks 693 | }; 694 | 695 | $.fn.fracs = function main() { 696 | var method = methods.fracs; 697 | 698 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 699 | args[_key] = arguments[_key]; 700 | } 701 | 702 | if (is_fn(methods[args[0]])) { 703 | method = methods[args.shift()]; 704 | } 705 | 706 | return Reflect.apply(method, this, args); 707 | }; 708 | })(); -------------------------------------------------------------------------------- /dist/jquery.fracs.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery-fracs v1.0.2 - https://larsjung.de/jquery-fracs/ */ 2 | "use strict";function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}!function(){function r(t,n){return _typeof(t)===n}function s(t,n){return t instanceof n}function l(t){return t&&t.nodeType}function h(t){return l(t)?t:s(t,c)?t[0]:void 0}function n(t,n,e){if(t===n)return!0;if(!t||!n||t.constructor!==n.constructor)return!1;for(var i=0,o=e.length;i { 14 | runtime.pkg = Object.assign({}, require('./package.json')); 15 | runtime.comment = `${runtime.pkg.name} v${runtime.pkg.version} - ${runtime.pkg.homepage}`; 16 | runtime.commentJs = `/*! ${runtime.comment} */\n`; 17 | 18 | console.log(runtime.comment); 19 | }); 20 | 21 | ghu.task('clean', 'delete build folder', () => { 22 | return remove(`${BUILD}, ${DIST}`); 23 | }); 24 | 25 | ghu.task('build:scripts', runtime => { 26 | return read(`${SRC}/*.js`) 27 | .then(babel({presets: ['@babel/preset-env']})) 28 | .then(wrap(runtime.commentJs)) 29 | .then(write(mapfn.p(SRC, DIST), {overwrite: true})) 30 | .then(write(mapfn.p(SRC, BUILD).s('.js', `-${runtime.pkg.version}.js`), {overwrite: true})) 31 | .then(uglify()) 32 | .then(wrap(runtime.commentJs)) 33 | .then(write(mapfn.p(SRC, DIST).s('.js', '.min.js'), {overwrite: true})) 34 | .then(write(mapfn.p(SRC, BUILD).s('.js', `-${runtime.pkg.version}.min.js`), {overwrite: true})); 35 | }); 36 | 37 | ghu.task('build:other', runtime => { 38 | return Promise.all([ 39 | read(`${SRC}/**/*.pug`) 40 | .then(pug({pkg: runtime.pkg})) 41 | .then(write(mapfn.p(SRC, BUILD).s('.pug', ''), {overwrite: true})), 42 | read(`${SRC}/**/*.less`) 43 | .then(includeit()) 44 | .then(less()) 45 | .then(cssmin()) 46 | .then(write(mapfn.p(SRC, BUILD).s('.less', '.css'), {overwrite: true})), 47 | read(`${SRC}/demo/*.js, ${SRC}/test/*.js`) 48 | .then(babel({presets: ['@babel/preset-env']})) 49 | .then(uglify()) 50 | .then(wrap(runtime.commentJs)) 51 | .then(write(mapfn.p(SRC, BUILD), {overwrite: true})), 52 | 53 | read(`${ROOT}/node_modules/scar/dist/scar.min.js`) 54 | .then(write(`${BUILD}/test/scar.min.js`, {overwrite: true})), 55 | read(`${ROOT}/node_modules/jquery/dist/jquery.min.js`) 56 | .then(write(`${BUILD}/demo/jquery.min.js`, {overwrite: true})) 57 | .then(write(`${BUILD}/test/jquery.min.js`, {overwrite: true})), 58 | 59 | read(`${ROOT}/*.md`) 60 | .then(write(mapfn.p(ROOT, BUILD), {overwrite: true})) 61 | ]); 62 | }); 63 | 64 | ghu.task('build', ['build:scripts', 'build:other']); 65 | 66 | ghu.task('zip', ['build'], runtime => { 67 | return read(`${BUILD}/**/*`) 68 | .then(jszip({dir: BUILD, level: 9})) 69 | .then(write(`${BUILD}/${NAME}-${runtime.pkg.version}.zip`, {overwrite: true})); 70 | }); 71 | 72 | ghu.task('release', ['clean', 'zip']); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-fracs", 3 | "title": "jQuery.fracs", 4 | "version": "1.0.2", 5 | "description": "Determine the visible fractions of HTML elements.", 6 | "homepage": "https://larsjung.de/jquery-fracs/", 7 | "author": "Lars Jung (https://larsjung.de)", 8 | "license": "MIT", 9 | "repository": "github:lrsjng/jquery-fracs", 10 | "scripts": { 11 | "lint": "eslint .", 12 | "build": "node ghu.js release", 13 | "precommit": "npm run -s lint && npm run -s build" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "7.10.5", 17 | "@babel/preset-env": "7.10.4", 18 | "eslint": "7.5.0", 19 | "ghu": "0.26.0", 20 | "jquery": "3.5.1", 21 | "normalize.css": "8.0.1", 22 | "scar": "2.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/demo/index.html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset='utf-8') 5 | title #{pkg.title} #{pkg.version} Demo 6 | meta(name='description', content=`Demo for ${pkg.title} (${pkg.homepage})`) 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | link(href='main.css', rel='stylesheet') 9 | 10 | body 11 | div#panel 12 | h1 13 | a(href=`${pkg.homepage}` title=`${pkg.title} project page`) #{pkg.title} 14 | | #{pkg.version} Demo 15 | 16 | section 17 | ul#fracs 18 | li.header 19 | span.idx # 20 | span.info possible 21 | span.info visible 22 | span.info viewport 23 | 24 | h2 Dimensions 25 | ul#dims 26 | li #[span doc:]#[span.info.doc undef.] 27 | li #[span viewport:]#[span.info.vp undef.] 28 | 29 | h2 ScrollState 30 | ul#scrollstate 31 | li #[span width:]#[span.info.width undef.] 32 | li #[span height:]#[span.info.height undef.] 33 | li #[span left:]#[span.info.left undef.] 34 | li #[span top:]#[span.info.top undef.] 35 | li #[span right:]#[span.info.right undef.] 36 | li #[span bottom:]#[span.info.bottom undef.] 37 | 38 | script(src='jquery.min.js') 39 | script(src=`../jquery.fracs-${pkg.version}.js`) 40 | script(src='main.js') 41 | -------------------------------------------------------------------------------- /src/demo/main.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const WIN = window // eslint-disable-line 3 | const $ = WIN.jQuery; 4 | 5 | const round = Math.round; 6 | 7 | const generate_content = () => { 8 | const $body = $('body'); 9 | const $panel_fracs = $('#fracs'); 10 | const get_scrollto_fn = $target => () => $target.fracs('scrollToThis', 50, 50, 500); 11 | 12 | for (let i = 1; i <= 9; i += 1) { 13 | const $section = $('
') 14 | .appendTo($body) 15 | .width(i * 300) 16 | .height(round(Math.random() * 600 + 100)); 17 | 18 | const $label = $('
#' + i + '
') 19 | .appendTo($section); 20 | 21 | $('
    ') 22 | .appendTo($label) 23 | .append($('
  • of max possible visibility
  • ')) 24 | .append($('
  • visible
  • ')) 25 | .append($('
  • of viewport
  • ')) 26 | .append($('
  • visible rect WxH:
  • ')) 27 | .append($('
  • document space L/T:
  • ')) 28 | .append($('
  • element space L/T:
  • ')) 29 | .append($('
  • viewport space L/T:
  • ')); 30 | 31 | // panel 32 | const $li = $('
  • ') 33 | .appendTo($panel_fracs) 34 | .append($('' + i + '')) 35 | .append($('')); 36 | 37 | $section.add($li).click(get_scrollto_fn($section)); 38 | $section.data('panel', $li); 39 | } 40 | }; 41 | 42 | const init_fracs_demo = () => { 43 | $('.box').fracs(function cb(fracs) { 44 | const $section = $(this); 45 | const $panel = $section.data('panel'); 46 | const $label = $section.find('.label'); 47 | 48 | $panel.find('.idx') 49 | .css('color', fracs.possible > 0.4 ? '#fff' : 'inherit'); 50 | 51 | $section.add($panel.find('.idx')) 52 | .css('background-color', 'rgba(29,119,194,' + fracs.possible + ')'); 53 | 54 | $panel.add($label) 55 | .find('.visible').text(round(fracs.visible * 100) + '%').end() 56 | .find('.viewport').text(round(fracs.viewport * 100) + '%').end() 57 | .find('.possible').text(round(fracs.possible * 100) + '%'); 58 | 59 | if (!fracs.rects) { 60 | $label.find('.rects').text('undefined'); 61 | } else { 62 | $label 63 | .find('.dims').text(fracs.rects.document.width + 'x' + fracs.rects.document.height).end() 64 | .find('.rect').text(fracs.rects.document.left + '/' + fracs.rects.document.top).end() 65 | .find('.rectElementSpace').text(fracs.rects.element.left + '/' + fracs.rects.element.top).end() 66 | .find('.rectViewportSpace').text(fracs.rects.viewport.left + '/' + fracs.rects.viewport.top).end() 67 | .stop(true) 68 | .animate({ 69 | left: fracs.rects.element.left + 'px', 70 | top: fracs.rects.element.top + 'px' 71 | }, 100); 72 | } 73 | }); 74 | 75 | $('.box').fracs('check'); 76 | 77 | $('#box-6') 78 | .fracs('unbind') 79 | .find('.label').empty().append('#6 (unbound)'); 80 | }; 81 | 82 | const init_scrollstate_demo = () => { 83 | $(WIN).fracs('scrollState', state => { 84 | ['width', 'height', 'left', 'top', 'right', 'bottom'].forEach(x => { 85 | const val = state[x]; 86 | const txt = isNaN(val) ? 'undef.' : x === 'width' || x === 'height' ? round(val * 100) + '%' : val + 'px'; 87 | $('#scrollstate .' + x).text(txt); 88 | }); 89 | }); 90 | }; 91 | 92 | const init_dims_demo = () => { 93 | const on_resize = () => { 94 | const doc = $(WIN).fracs('content'); 95 | const vp = $(WIN).fracs('viewport'); 96 | $('.doc').text(doc.width + 'x' + doc.height); 97 | $('.vp').text(vp.width + 'x' + vp.height); 98 | }; 99 | 100 | $(WIN).bind('resize', on_resize); 101 | on_resize(); 102 | }; 103 | 104 | const init = () => { 105 | generate_content(); 106 | init_fracs_demo(); 107 | init_scrollstate_demo(); 108 | init_dims_demo(); 109 | }; 110 | 111 | $(init); 112 | })(); 113 | -------------------------------------------------------------------------------- /src/demo/main.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // @include "../../node_modules/normalize.css/normalize.css" 4 | 5 | body { 6 | font-family: "Ubuntu Mono", monospace; 7 | font-size: 16px; 8 | color: #333; 9 | position: absolute; 10 | } 11 | 12 | a, a:visited, a:active { 13 | color: #1d77c2; 14 | text-decoration: none; 15 | 16 | &:hover { 17 | color: #555; 18 | } 19 | } 20 | 21 | .box { 22 | position: relative; 23 | margin: 30px; 24 | border: 2px solid #333; 25 | height: 250px; 26 | overflow: hidden; 27 | cursor: pointer; 28 | 29 | .label { 30 | position: relative; 31 | display: inline-block; 32 | overflow: hidden; 33 | margin: 10px; 34 | padding: 8px; 35 | border-radius: 4px; 36 | background-color: rgba(255,255,255,0.5); 37 | } 38 | 39 | .idx { 40 | font-weight: bold; 41 | font-size: 1.8em; 42 | color: rgba(0,0,0,0.7); 43 | } 44 | 45 | ul { 46 | list-style: none; 47 | margin: 1em 0 0 0; 48 | padding: 0; 49 | } 50 | 51 | li { 52 | margin: 0.4em 0; 53 | } 54 | } 55 | 56 | #panel { 57 | font-size: 0.85em; 58 | position: fixed; 59 | top: 8px; 60 | right: 8px; 61 | background-color: #fff; 62 | z-index: 1; 63 | border: 2px solid #555; 64 | padding: 8px; 65 | width: 320px; 66 | 67 | h1 { 68 | font-size: 1.2em; 69 | text-align: center; 70 | margin: 0.6em 0 1em 0; 71 | } 72 | 73 | h2 { 74 | font-size: 1em; 75 | text-align: left; 76 | margin: 1em 0 0.6em 0; 77 | } 78 | } 79 | 80 | #fracs { 81 | list-style: none; 82 | margin: 0; 83 | padding: 0; 84 | 85 | li { 86 | margin: 0; 87 | padding: 0; 88 | cursor: pointer; 89 | margin-bottom: 2px; 90 | 91 | } 92 | 93 | .header { 94 | font-weight: bold; 95 | margin-bottom: 4px; 96 | } 97 | 98 | span { 99 | display: inline-block; 100 | margin: 0; 101 | padding: 0; 102 | width: 94px; 103 | text-align: right; 104 | } 105 | 106 | .idx { 107 | width: 30px; 108 | text-align: center; 109 | } 110 | } 111 | 112 | #dims, #scrollstate { 113 | list-style: none; 114 | margin: 0; 115 | padding: 0; 116 | 117 | li { 118 | display: inline-block; 119 | margin: 0 8px; 120 | width: 144px; 121 | } 122 | 123 | span { 124 | display: inline-block; 125 | margin: 0; 126 | padding: 0; 127 | width: 50%; 128 | overflow: hidden; 129 | } 130 | 131 | .info { 132 | text-align: right; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/jquery.fracs.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const WIN = window; // eslint-disable-line 3 | const DOC = WIN.document; 4 | const $ = WIN.jQuery; 5 | const $WIN = $(WIN); 6 | const $DOC = $(DOC); 7 | const extend = $.extend; 8 | const is_fn = $.isFunction; 9 | const math_max = Math.max; 10 | const math_min = Math.min; 11 | const math_round = Math.round; 12 | const is_typeof = (obj, type) => typeof obj === type; 13 | const is_instanceof = (obj, type) => obj instanceof type; 14 | const is_html_el = obj => obj && obj.nodeType; 15 | const get_html_el = obj => is_html_el(obj) ? obj : is_instanceof(obj, $) ? obj[0] : undefined; 16 | 17 | const get_id = (() => { 18 | const ids = {}; 19 | let next_id = 1; 20 | 21 | return el => { 22 | if (!el) { 23 | return 0; 24 | } 25 | if (!ids[el]) { 26 | ids[el] = next_id; 27 | next_id += 1; 28 | } 29 | return ids[el]; 30 | }; 31 | })(); 32 | 33 | const equal = (x, y, props) => { 34 | if (x === y) { 35 | return true; 36 | } 37 | if (!x || !y || x.constructor !== y.constructor) { 38 | return false; 39 | } 40 | for (let i = 0, l = props.length; i < l; i += 1) { 41 | const prop = props[i]; 42 | if (x[prop] && is_fn(x[prop].equals) && !x[prop].equals(y[prop])) { 43 | return false; 44 | } 45 | if (x[prop] !== y[prop]) { 46 | return false; 47 | } 48 | } 49 | return true; 50 | }; 51 | 52 | 53 | 54 | 55 | function Rect(left, top, width, height) { 56 | this.left = math_round(left); 57 | this.top = math_round(top); 58 | this.width = math_round(width); 59 | this.height = math_round(height); 60 | this.right = this.left + this.width; 61 | this.bottom = this.top + this.height; 62 | } 63 | 64 | extend(Rect.prototype, { 65 | equals(that) { 66 | return equal(this, that, ['left', 'top', 'width', 'height']); 67 | }, 68 | 69 | area() { 70 | return this.width * this.height; 71 | }, 72 | 73 | relativeTo(rect) { 74 | return new Rect(this.left - rect.left, this.top - rect.top, this.width, this.height); 75 | }, 76 | 77 | intersection(rect) { 78 | if (!is_instanceof(rect, Rect)) { 79 | return null; 80 | } 81 | 82 | const left = math_max(this.left, rect.left); 83 | const right = math_min(this.right, rect.right); 84 | const top = math_max(this.top, rect.top); 85 | const bottom = math_min(this.bottom, rect.bottom); 86 | const width = right - left; 87 | const height = bottom - top; 88 | 89 | return width >= 0 && height >= 0 ? new Rect(left, top, width, height) : null; 90 | }, 91 | 92 | envelope(rect) { 93 | if (!is_instanceof(rect, Rect)) { 94 | return this; 95 | } 96 | 97 | const left = math_min(this.left, rect.left); 98 | const right = math_max(this.right, rect.right); 99 | const top = math_min(this.top, rect.top); 100 | const bottom = math_max(this.bottom, rect.bottom); 101 | const width = right - left; 102 | const height = bottom - top; 103 | 104 | return new Rect(left, top, width, height); 105 | } 106 | }); 107 | 108 | extend(Rect, { 109 | ofContent(el, in_content_space) { 110 | if (!el || el === DOC || el === WIN) { 111 | return new Rect(0, 0, $DOC.width(), $DOC.height()); 112 | } 113 | 114 | if (in_content_space) { 115 | return new Rect(0, 0, el.scrollWidth, el.scrollHeight); 116 | } 117 | 118 | return new Rect(el.offsetLeft - el.scrollLeft, el.offsetTop - el.scrollTop, el.scrollWidth, el.scrollHeight); 119 | }, 120 | 121 | ofViewport(el, in_content_space) { 122 | if (!el || el === DOC || el === WIN) { 123 | return new Rect($WIN.scrollLeft(), $WIN.scrollTop(), $WIN.width(), $WIN.height()); 124 | } 125 | 126 | if (in_content_space) { 127 | return new Rect(el.scrollLeft, el.scrollTop, el.clientWidth, el.clientHeight); 128 | } 129 | 130 | return new Rect(el.offsetLeft, el.offsetTop, el.clientWidth, el.clientHeight); 131 | }, 132 | 133 | ofElement(el) { 134 | const $el = $(el); 135 | if (!$el.is(':visible')) { 136 | return null; 137 | } 138 | 139 | const offset = $el.offset(); 140 | return new Rect(offset.left, offset.top, $el.outerWidth(), $el.outerHeight()); 141 | } 142 | }); 143 | 144 | 145 | 146 | 147 | function Fractions(visible, viewport, possible, rects) { 148 | this.visible = visible || 0; 149 | this.viewport = viewport || 0; 150 | this.possible = possible || 0; 151 | this.rects = rects && extend({}, rects) || null; 152 | } 153 | 154 | extend(Fractions.prototype, { 155 | equals(that) { 156 | return this.fracsEqual(that) && this.rectsEqual(that); 157 | }, 158 | 159 | fracsEqual(that) { 160 | return equal(this, that, ['visible', 'viewport', 'possible']); 161 | }, 162 | 163 | rectsEqual(that) { 164 | return equal(this.rects, that.rects, ['document', 'element', 'viewport']); 165 | } 166 | }); 167 | 168 | extend(Fractions, { 169 | of(rect, viewport) { 170 | rect = is_html_el(rect) && Rect.ofElement(rect) || rect; 171 | viewport = is_html_el(viewport) && Rect.ofViewport(viewport) || viewport || Rect.ofViewport(); 172 | 173 | if (!is_instanceof(rect, Rect)) { 174 | return new Fractions(); 175 | } 176 | 177 | const intersection = rect.intersection(viewport); 178 | if (!intersection) { 179 | return new Fractions(); 180 | } 181 | 182 | const intersection_area = intersection.area(); 183 | const possible_area = math_min(rect.width, viewport.width) * math_min(rect.height, viewport.height); 184 | return new Fractions( 185 | intersection_area / rect.area(), 186 | intersection_area / viewport.area(), 187 | intersection_area / possible_area, 188 | { 189 | document: intersection, 190 | element: intersection.relativeTo(rect), 191 | viewport: intersection.relativeTo(viewport) 192 | } 193 | ); 194 | } 195 | }); 196 | 197 | 198 | 199 | 200 | function Group(els, viewport) { 201 | this.els = els; 202 | this.viewport = viewport; 203 | } 204 | 205 | const RECT_PROPS = ['width', 'height', 'left', 'right', 'top', 'bottom']; 206 | const FRACS_PROPS = ['possible', 'visible', 'viewport']; 207 | 208 | const get_value = (el, viewport, prop) => { 209 | let obj; 210 | if (RECT_PROPS.includes(prop)) { 211 | obj = Rect.ofElement(el); 212 | } else if (FRACS_PROPS.includes(prop)) { 213 | obj = Fractions.of(el, viewport); 214 | } 215 | return obj ? obj[prop] : 0; 216 | }; 217 | 218 | const sort_asc = (x, y) => x.val - y.val; 219 | const sort_desc = (x, y) => y.val - x.val; 220 | 221 | extend(Group.prototype, { 222 | sorted(prop, desc) { 223 | const viewport = this.viewport; 224 | 225 | return $.map(this.els, el => { 226 | return { 227 | el, 228 | val: get_value(el, viewport, prop) 229 | }; 230 | }).sort(desc ? sort_desc : sort_asc); 231 | }, 232 | 233 | best(prop, desc) { 234 | return this.els.length ? this.sorted(prop, desc)[0] : null; 235 | } 236 | }); 237 | 238 | 239 | 240 | 241 | function ScrollState(el) { 242 | const content = Rect.ofContent(el, true); 243 | const viewport = Rect.ofViewport(el, true); 244 | const w = content.width - viewport.width; 245 | const h = content.height - viewport.height; 246 | 247 | this.content = content; 248 | this.viewport = viewport; 249 | this.width = w <= 0 ? null : viewport.left / w; 250 | this.height = h <= 0 ? null : viewport.top / h; 251 | this.left = viewport.left; 252 | this.top = viewport.top; 253 | this.right = content.right - viewport.right; 254 | this.bottom = content.bottom - viewport.bottom; 255 | } 256 | 257 | extend(ScrollState.prototype, { 258 | equals(that) { 259 | return equal(this, that, ['width', 'height', 'left', 'top', 'right', 'bottom', 'content', 'viewport']); 260 | } 261 | }); 262 | 263 | 264 | 265 | 266 | function Viewport(el) { 267 | this.el = el || WIN; 268 | } 269 | 270 | extend(Viewport.prototype, { 271 | equals(that) { 272 | return equal(this, that, ['el']); 273 | }, 274 | 275 | scrollState() { 276 | return new ScrollState(this.el); 277 | }, 278 | 279 | scrollTo(left, top, duration) { 280 | const $el = this.el === WIN ? $('html,body') : $(this.el); 281 | left = left || 0; 282 | top = top || 0; 283 | duration = isNaN(duration) ? 1000 : duration; 284 | $el.stop(true).animate({scrollLeft: left, scrollTop: top}, duration); 285 | }, 286 | 287 | scrollToRect(rect, left, top, duration) { 288 | left = left || 0; 289 | top = top || 0; 290 | this.scrollTo(rect.left - left, rect.top - top, duration); 291 | }, 292 | 293 | scrollToElement(el, left, top, duration) { 294 | const rect = Rect.ofElement(el).relativeTo(Rect.ofContent(this.el)); 295 | this.scrollToRect(rect, left, top, duration); 296 | } 297 | }); 298 | 299 | 300 | 301 | 302 | const callback_mixin = { 303 | context: null, 304 | updatedValue: () => null, 305 | 306 | init(target) { 307 | this.callbacks = $.Callbacks('memory unique'); 308 | this.curr_val = null; 309 | this.prev_val = null; 310 | $(target || WIN).on('load resize scroll', $.proxy(this.check, this)); 311 | }, 312 | 313 | bind(callback) { 314 | this.callbacks.add(callback); 315 | }, 316 | 317 | unbind(callback) { 318 | if (callback) { 319 | this.callbacks.remove(callback); 320 | } else { 321 | this.callbacks.empty(); 322 | } 323 | }, 324 | 325 | check(event) { 326 | const val = this.updatedValue(event); 327 | if (val === undefined) { 328 | return false; 329 | } 330 | 331 | this.prev_val = this.curr_val; 332 | this.curr_val = val; 333 | this.callbacks.fireWith(this.context, [this.curr_val, this.prev_val]); 334 | return true; 335 | } 336 | }; 337 | 338 | 339 | function FracsCallbacks(el, viewport) { 340 | this.context = el; 341 | this.viewport = viewport; 342 | this.init(); 343 | } 344 | 345 | extend(FracsCallbacks.prototype, callback_mixin, { 346 | updatedValue() { 347 | const val = Fractions.of(this.context, this.viewport); 348 | if (!val.equals(this.curr_val)) { 349 | return val; 350 | } 351 | return undefined; 352 | } 353 | }); 354 | 355 | 356 | function GroupCallbacks(els, viewport, prop, desc) { 357 | this.context = new Group(els, viewport); 358 | this.property = prop; 359 | this.descending = desc; 360 | this.init(); 361 | } 362 | 363 | extend(GroupCallbacks.prototype, callback_mixin, { 364 | updatedValue() { 365 | let best = this.context.best(this.property, this.descending); 366 | if (best) { 367 | best = best.val > 0 ? best.el : null; 368 | if (this.curr_val !== best) { 369 | return best; 370 | } 371 | } 372 | return undefined; 373 | } 374 | }); 375 | 376 | 377 | function ScrollStateCallbacks(el) { 378 | if (!el || el === WIN || el === DOC) { 379 | this.context = WIN; 380 | } else { 381 | this.context = el; 382 | } 383 | this.init(this.context); 384 | } 385 | 386 | extend(ScrollStateCallbacks.prototype, callback_mixin, { 387 | updatedValue() { 388 | const val = new ScrollState(this.context); 389 | if (!val.equals(this.curr_val)) { 390 | return val; 391 | } 392 | return undefined; 393 | } 394 | }); 395 | 396 | 397 | 398 | 399 | // # Public API 400 | // accessible via `$(selector).fracs('', ...)`. 401 | 402 | const methods = { 403 | // ## 'content' 404 | // Returns the content rect of the first selected element in content space. 405 | // If no element is selected it returns the document rect. 406 | content(in_content_space) { 407 | return this.length ? Rect.ofContent(this[0], in_content_space) : null; 408 | }, 409 | 410 | // ## 'envelope' 411 | // Returns the smallest rectangle that containes all selected elements. 412 | envelope() { 413 | let res; 414 | $.each(this, (idx, el) => { 415 | const rect = Rect.ofElement(el); 416 | res = res ? res.envelope(rect) : rect; 417 | }); 418 | return res; 419 | }, 420 | 421 | // ## 'fracs' 422 | // This is the **default method**. So the first parameter `'fracs'` 423 | // can be omitted. 424 | // 425 | // Returns the fractions for the first selected element. 426 | // 427 | // .fracs(): Fractions 428 | // 429 | // Binds a callback function that will be invoked if fractions have changed 430 | // after a `window resize` or `window scroll` event. 431 | // 432 | // .fracs(callback(fracs: Fractions, prev_fracs: Fractions)): jQuery 433 | // 434 | // Unbinds the specified callback function. 435 | // 436 | // .fracs('unbind', callback): jQuery 437 | // 438 | // Unbinds all callback functions. 439 | // 440 | // .fracs('unbind'): jQuery 441 | // 442 | // Checks if fractions changed and if so invokes all bound callback functions. 443 | // 444 | // .fracs('check'): jQuery 445 | fracs(action, callback, viewport) { 446 | if (!is_typeof(action, 'string')) { 447 | viewport = callback; 448 | callback = action; 449 | action = null; 450 | } 451 | if (!is_fn(callback)) { 452 | viewport = callback; 453 | callback = null; 454 | } 455 | viewport = get_html_el(viewport); 456 | 457 | const ns = 'fracs.' + get_id(viewport); 458 | 459 | if (action === 'unbind') { 460 | return this.each(function cb() { 461 | const cbs = $(this).data(ns); 462 | if (cbs) { 463 | cbs.unbind(callback); 464 | } 465 | }); 466 | } else if (action === 'check') { 467 | return this.each(function cb() { 468 | const cbs = $(this).data(ns); 469 | if (cbs) { 470 | cbs.check(); 471 | } 472 | }); 473 | } else if (is_fn(callback)) { 474 | return this.each(function cb() { 475 | const $this = $(this); 476 | let cbs = $this.data(ns); 477 | if (!cbs) { 478 | cbs = new FracsCallbacks(this, viewport); 479 | $this.data(ns, cbs); 480 | } 481 | cbs.bind(callback); 482 | }); 483 | } 484 | 485 | return this.length ? Fractions.of(this[0], viewport) : null; 486 | }, 487 | 488 | // ## 'intersection' 489 | // Returns the greatest rectangle that is contained in all selected elements. 490 | intersection() { 491 | let res; 492 | $.each(this, (idx, el) => { 493 | const rect = Rect.ofElement(el); 494 | res = res ? res.intersection(rect) : rect; 495 | }); 496 | return res; 497 | }, 498 | 499 | // ## 'max' 500 | // Reduces the set of selected elements to those with the maximum value 501 | // of the specified property. 502 | // Valid values for property are `possible`, `visible`, `viewport`, 503 | // `width`, `height`, `left`, `right`, `top`, `bottom`. 504 | // 505 | // .fracs('max', property: String): jQuery 506 | // 507 | // Binds a callback function to the set of selected elements that gets 508 | // triggert whenever the element with the highest value of the specified 509 | // property changes. 510 | // 511 | // .fracs('max', property: String, callback(best: Element, prev_best: Element)): jQuery 512 | max(prop, callback, viewport) { 513 | if (!is_fn(callback)) { 514 | viewport = callback; 515 | callback = null; 516 | } 517 | viewport = get_html_el(viewport); 518 | 519 | if (callback) { 520 | new GroupCallbacks(this, viewport, prop, true).bind(callback); 521 | return this; 522 | } 523 | 524 | return this.pushStack(new Group(this, viewport).best(prop, true).el); 525 | }, 526 | 527 | // ## 'min' 528 | // Reduces the set of selected elements to those with the minimum value 529 | // of the specified property. 530 | // Valid values for property are `possible`, `visible`, `viewport`, 531 | // `width`, `height`, `left`, `right`, `top`, `bottom`. 532 | // 533 | // .fracs('min', property: String): jQuery 534 | // 535 | // Binds a callback function to the set of selected elements that gets 536 | // triggert whenever the element with the lowest value of the specified 537 | // property changes. 538 | // 539 | // .fracs('min', property: String, callback(best: Element, prev_best: Element)): jQuery 540 | min(prop, callback, viewport) { 541 | if (!is_fn(callback)) { 542 | viewport = callback; 543 | callback = null; 544 | } 545 | viewport = get_html_el(viewport); 546 | 547 | if (callback) { 548 | new GroupCallbacks(this, viewport, prop).bind(callback); 549 | return this; 550 | } 551 | 552 | return this.pushStack(new Group(this, viewport).best(prop).el); 553 | }, 554 | 555 | // ## 'rect' 556 | // Returns the dimensions for the first selected element in document space. 557 | rect() { 558 | return this.length ? Rect.ofElement(this[0]) : null; 559 | }, 560 | 561 | // ## 'scrollState' 562 | // Returns the current scroll state for the first selected element. 563 | // 564 | // .fracs('scrollState'): ScrollState 565 | // 566 | // Binds a callback function that will be invoked if scroll state has changed 567 | // after a `resize` or `scroll` event. 568 | // 569 | // .fracs('scrollState', callback(scrollState: scrollState, prevScrollState: scrollState)): jQuery 570 | // 571 | // Unbinds the specified callback function. 572 | // 573 | // .fracs('scrollState', 'unbind', callback): jQuery 574 | // 575 | // Unbinds all callback functions. 576 | // 577 | // .fracs('scrollState', 'unbind'): jQuery 578 | // 579 | // Checks if scroll state changed and if so invokes all bound callback functions. 580 | // 581 | // .fracs('scrollState', 'check'): jQuery 582 | scrollState(action, callback) { 583 | const ns = 'fracs.scrollState'; 584 | 585 | if (!is_typeof(action, 'string')) { 586 | callback = action; 587 | action = null; 588 | } 589 | 590 | if (action === 'unbind') { 591 | return this.each(function cb() { 592 | const cbs = $(this).data(ns); 593 | if (cbs) { 594 | cbs.unbind(callback); 595 | } 596 | }); 597 | } else if (action === 'check') { 598 | return this.each(function cb() { 599 | const cbs = $(this).data(ns); 600 | if (cbs) { 601 | cbs.check(); 602 | } 603 | }); 604 | } else if (is_fn(callback)) { 605 | return this.each(function cb() { 606 | const $this = $(this); 607 | let cbs = $this.data(ns); 608 | if (!cbs) { 609 | cbs = new ScrollStateCallbacks(this); 610 | $this.data(ns, cbs); 611 | } 612 | cbs.bind(callback); 613 | }); 614 | } 615 | 616 | return this.length ? new ScrollState(this[0]) : null; 617 | }, 618 | 619 | // ## 'scroll' 620 | // Scrolls the selected elements relative to its current position, 621 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 622 | // 623 | // .fracs('scroll', element: HTMLElement/jQuery, [left: int,] [top: int,] [duration: int]): jQuery 624 | scroll(left, top, duration) { 625 | return this.each(function cb() { 626 | new Viewport(this).scroll(left, top, duration); 627 | }); 628 | }, 629 | 630 | // ## 'scrollTo' 631 | // Scrolls the selected elements to the specified element or an absolute position, 632 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 633 | // 634 | // .fracs('scrollTo', element: HTMLElement/jQuery, [left: int,] [top: int,] [duration: int]): jQuery 635 | // .fracs('scrollTo', [left: int,] [top: int,] [duration: int]): jQuery 636 | scrollTo(el, left, top, duration) { 637 | if ($.isNumeric(el)) { 638 | duration = top; 639 | top = left; 640 | left = el; 641 | el = null; 642 | } 643 | 644 | el = get_html_el(el); 645 | 646 | return this.each(function cb() { 647 | if (el) { 648 | new Viewport(this).scrollToElement(el, left, top, duration); 649 | } else { 650 | new Viewport(this).scrollTo(left, top, duration); 651 | } 652 | }); 653 | }, 654 | 655 | // ## 'scrollToThis' 656 | // Scrolls the viewport (defaults to window) to the first selected element in the specified time, 657 | // `left` and `top` paddings default to `0`, `duration` to `1000`. 658 | scrollToThis(left, top, duration, viewport) { 659 | viewport = new Viewport(get_html_el(viewport)); 660 | viewport.scrollToElement(this[0], left, top, duration); 661 | return this; 662 | }, 663 | 664 | // ## 'sort' 665 | // Sorts the set of selected elements by the specified prop. 666 | // Valid values for prop are `possible`, `visible`, `viewport`, 667 | // `width`, `height`, `left`, `right`, `top`, `bottom`. The default 668 | // sort order is descending. 669 | sort(prop, ascending, viewport) { 670 | if (!is_typeof(ascending, 'boolean')) { 671 | viewport = ascending; 672 | ascending = null; 673 | } 674 | viewport = get_html_el(viewport); 675 | 676 | return this.pushStack($.map(new Group(this, viewport).sorted(prop, !ascending), entry => entry.el)); 677 | }, 678 | 679 | // ## 'viewport' 680 | // Returns the current viewport of the first selected element. 681 | // If no element is selected it returns the document's viewport. 682 | viewport(in_content_space) { 683 | return this.length ? Rect.ofViewport(this[0], in_content_space) : null; 684 | } 685 | }; 686 | 687 | 688 | 689 | 690 | $.fracs = (rect, viewport) => Fractions.of(rect, viewport); 691 | 692 | $.fracs._ = { // published for testing 693 | Rect, 694 | Fractions, 695 | Group, 696 | ScrollState, 697 | Viewport, 698 | FracsCallbacks, 699 | GroupCallbacks, 700 | ScrollStateCallbacks 701 | }; 702 | 703 | $.fn.fracs = function main(...args) { 704 | let method = methods.fracs; 705 | if (is_fn(methods[args[0]])) { 706 | method = methods[args.shift()]; 707 | } 708 | return Reflect.apply(method, this, args); 709 | }; 710 | })(); 711 | -------------------------------------------------------------------------------- /src/test/index.html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset='utf-8') 5 | title #{pkg.title} #{pkg.version} Test Suite 6 | meta(name='description', content=`Test suite for ${pkg.title} (${pkg.homepage})`) 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | link(href='main.css', rel='stylesheet') 9 | 10 | body 11 | h1 #{pkg.title} #{pkg.version} Test Suite 12 | 13 | div#test-elements 14 | 15 | script(src='jquery.min.js') 16 | script(src=`../jquery.fracs-${pkg.version}.js`) 17 | script(src='scar.min.js') 18 | script(src='main.js') 19 | -------------------------------------------------------------------------------- /src/test/main.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const WIN = window; // eslint-disable-line 3 | const DOC = WIN.document; 4 | const $ = WIN.jQuery; 5 | const $WIN = $(WIN); 6 | const $DOC = $(DOC); 7 | const is_fn = $.isFunction; 8 | const by_id = id => DOC.getElementById(id); 9 | 10 | const test = WIN.scar.test; 11 | const assert = WIN.scar.assert; 12 | 13 | test('Plugin access', () => { 14 | assert.ok(is_fn($.fracs), '$.fracs is function'); 15 | assert.ok(is_fn($().fracs), '$().fracs is function'); 16 | 17 | assert.equal(Object.keys($.fracs).length, 1, '$.fracs has right number of members'); 18 | assert.equal(Object.keys($.fracs._).length, 8, '$.fracs._ has right number of members'); 19 | assert.ok(is_fn($.fracs._.Rect), '$.fracs._.Rect is function'); 20 | assert.ok(is_fn($.fracs._.Fractions), '$.fracs._.Fractions is function'); 21 | assert.ok(is_fn($.fracs._.Group), '$.fracs._.Group is function'); 22 | assert.ok(is_fn($.fracs._.ScrollState), '$.fracs._.ScrollState is function'); 23 | assert.ok(is_fn($.fracs._.Viewport), '$.fracs._.Viewport is function'); 24 | assert.ok(is_fn($.fracs._.FracsCallbacks), '$.fracs._.FracsCallbacks is function'); 25 | assert.ok(is_fn($.fracs._.GroupCallbacks), '$.fracs._.GroupCallbacks is function'); 26 | assert.ok(is_fn($.fracs._.ScrollStateCallbacks), '$.fracs._.ScrollStateCallbacks is function'); 27 | }); 28 | 29 | 30 | // Objects 31 | // ======= 32 | 33 | const Rect = $.fracs._.Rect; 34 | const Fractions = $.fracs._.Fractions; 35 | const create_test_el = (() => { 36 | let idx = 0; 37 | return css => { 38 | idx += 1; 39 | $('
    ') 40 | .addClass('box') 41 | .css(css) 42 | .text(idx) 43 | .appendTo($('#test-elements')); 44 | 45 | return Rect.ofElement(by_id('el-' + idx)); 46 | }; 47 | })(); 48 | 49 | 50 | // Rect 51 | // ---- 52 | 53 | test('Rect constructor', () => { 54 | const rect1 = new Rect(30, 50, 400, 300); 55 | const rect2 = new Rect(30.1, 50.4, 400.3, 299.5); 56 | 57 | assert.ok(rect1 instanceof Rect, 'instanceof Rect'); 58 | 59 | assert.equal(rect1.left, 30, 'left'); 60 | assert.equal(rect1.top, 50, 'top'); 61 | assert.equal(rect1.width, 400, 'width'); 62 | assert.equal(rect1.height, 300, 'height'); 63 | assert.equal(rect1.right, 430, 'right'); 64 | assert.equal(rect1.bottom, 350, 'bottom'); 65 | 66 | assert.equal(rect2.left, 30, 'left'); 67 | assert.equal(rect2.top, 50, 'top'); 68 | assert.equal(rect2.width, 400, 'width'); 69 | assert.equal(rect2.height, 300, 'height'); 70 | assert.equal(rect2.right, 430, 'right'); 71 | assert.equal(rect2.bottom, 350, 'bottom'); 72 | }); 73 | 74 | test('Rect equals', () => { 75 | const rect1 = new Rect(30, 50, 400, 300); 76 | const rect2 = new Rect(30.1, 50.4, 400.3, 299.5); 77 | const rect3 = new Rect(100, 200, 400, 300); 78 | 79 | assert.ok(!rect1.equals(), 'unequal to undefined'); 80 | assert.ok(!rect1.equals(null), 'unequal to null'); 81 | assert.ok(!rect1.equals({}), 'unequal to {}'); 82 | 83 | assert.ok(rect1.equals(rect2), 'equal rects'); 84 | assert.ok(!rect1.equals(rect3), 'unequal rects'); 85 | }); 86 | 87 | test('Rect area', () => { 88 | const rect1 = new Rect(30, 50, 400, 300); 89 | assert.equal(rect1.area(), 400 * 300, 'area'); 90 | }); 91 | 92 | test('Rect relativeTo', () => { 93 | const rect1 = new Rect(30, 50, 400, 300); 94 | const rect2 = new Rect(10, 10, 10, 10); 95 | const rect3 = new Rect(20, 40, 400, 300); 96 | 97 | assert.deepEqual(rect1.relativeTo(rect2), rect3, 'relativeTo'); 98 | }); 99 | 100 | test('Rect intersection', () => { 101 | const rect1 = new Rect(30, 50, 400, 300); 102 | const rect2 = new Rect(100, 200, 400, 300); 103 | const rect3 = new Rect(500, 200, 400, 300); 104 | const intersection = new Rect(100, 200, 330, 150); 105 | 106 | assert.deepEqual(rect1.intersection(rect2), intersection, 'intersection'); 107 | assert.deepEqual(rect1.intersection(rect3), null, 'no intersection'); 108 | assert.deepEqual(rect1.intersection(null), null, 'no second rect'); 109 | }); 110 | 111 | test('Rect envelope', () => { 112 | const rect1 = new Rect(30, 50, 400, 300); 113 | const rect2 = new Rect(100, 200, 400, 300); 114 | const envelope = new Rect(30, 50, 470, 450); 115 | 116 | assert.deepEqual(rect1.envelope(rect2), envelope, 'envelope'); 117 | assert.deepEqual(rect1.envelope(null), rect1, 'no second rect'); 118 | }); 119 | 120 | test('Rect ofContent', () => { 121 | const rect1 = Rect.ofContent(); 122 | const w = $DOC.width(); 123 | const h = $DOC.height(); 124 | 125 | assert.deepEqual(rect1, new Rect(0, 0, w, h), 'dims'); 126 | }); 127 | 128 | test('Rect ofViewport', () => { 129 | const rect1 = Rect.ofViewport(); 130 | const l = $WIN.scrollLeft(); 131 | const t = $WIN.scrollTop(); 132 | const w = $WIN.width(); 133 | const h = $WIN.height(); 134 | 135 | assert.deepEqual(rect1, new Rect(l, t, w, h), 'dims'); 136 | }); 137 | 138 | test('Rect ofElement', () => { 139 | const left = -100; 140 | let top = 0; 141 | let rect; 142 | 143 | top += 100; 144 | rect = create_test_el({left, top, width: 20, height: 30}); 145 | assert.deepEqual(rect, new Rect(left, top, 20, 30), 'dims'); 146 | 147 | top += 100; 148 | rect = create_test_el({left, top, width: 20, height: 30, padding: 1}); 149 | assert.deepEqual(rect, new Rect(left, top, 22, 32), 'padding'); 150 | 151 | top += 100; 152 | rect = create_test_el({left, top, width: 20, height: 30, paddingLeft: 1}); 153 | assert.deepEqual(rect, new Rect(left, top, 21, 30), 'one sided padding'); 154 | 155 | top += 100; 156 | rect = create_test_el({left, top, width: 20, height: 30, borderWidth: 2}); 157 | assert.deepEqual(rect, new Rect(left, top, 24, 34), 'border'); 158 | 159 | top += 100; 160 | rect = create_test_el({left, top, width: 20, height: 30, borderLeftWidth: 2}); 161 | assert.deepEqual(rect, new Rect(left, top, 22, 30), 'one sided border'); 162 | 163 | top += 100; 164 | rect = create_test_el({left, top, width: 20, height: 30, padding: 1, borderWidth: 2}); 165 | assert.deepEqual(rect, new Rect(left, top, 26, 36), 'padding and border'); 166 | 167 | top += 100; 168 | rect = create_test_el({left, top, width: 20, height: 30, display: 'none'}); 169 | assert.equal(rect, null, 'display: none; element returns null'); 170 | 171 | top += 100; 172 | rect = create_test_el({left, top, width: 20, height: 30, visibility: 'hidden'}); 173 | assert.deepEqual(rect, new Rect(left, top, 20, 30), 'visibility: hidden; element returns rect'); 174 | }); 175 | 176 | 177 | // Fractions 178 | // --------- 179 | 180 | test('Fractions constructor', () => { 181 | const fr = new Fractions(); 182 | 183 | assert.ok(fr instanceof Fractions, 'instanceof Fractions'); 184 | assert.equal(fr.visible, 0, 'visible'); 185 | assert.equal(fr.viewport, 0, 'viewport'); 186 | assert.equal(fr.possible, 0, 'possible'); 187 | assert.equal(fr.rects, null, 'rects'); 188 | }); 189 | 190 | test('Fractions equals', () => { 191 | const rect1 = new Rect(30, 50, 400, 300); 192 | const rect2 = new Rect(100, 200, 400, 300); 193 | const rect3 = new Rect(10, 20, 40, 30); 194 | const fr1 = new Fractions(0.1, 0.2, 0.3, {document: rect1, element: rect2, viewport: rect3}); 195 | const fr2 = new Fractions(0.1, 0.2, 0.3, {document: rect1, element: rect2, viewport: rect3}); 196 | const fr3 = new Fractions(0.1, 0.2, 0.3, {document: rect3, element: rect2, viewport: rect1}); 197 | const fr4 = new Fractions(0.2, 0.2, 0.3, {document: rect1, element: rect2, viewport: rect3}); 198 | 199 | assert.ok(!fr1.equals(), 'unequal to undefined'); 200 | assert.ok(!fr1.equals(null), 'unequal to null'); 201 | assert.ok(!fr1.equals({}), 'unequal to {}'); 202 | 203 | assert.ok(fr1.equals(fr2), 'equal'); 204 | assert.ok(!fr1.equals(fr3), 'unequal'); 205 | assert.ok(!fr1.equals(fr4), 'unequal'); 206 | 207 | assert.ok(fr1.fracsEqual(fr2), 'fracs equal'); 208 | assert.ok(fr1.fracsEqual(fr3), 'fracs equal'); 209 | assert.ok(!fr1.fracsEqual(fr4), 'fracs unequal'); 210 | 211 | assert.ok(fr1.rectsEqual(fr2), 'rects equal'); 212 | assert.ok(!fr1.rectsEqual(fr3), 'rects unequal'); 213 | assert.ok(fr1.rectsEqual(fr4), 'rects equal'); 214 | }); 215 | 216 | 217 | test.cli(); 218 | })(); 219 | -------------------------------------------------------------------------------- /src/test/main.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // @include "../../node_modules/normalize.css/normalize.css" 4 | 5 | h1 { 6 | margin: 16px; 7 | font-family: "Ubuntu Mono", monospace; 8 | font-size: 20px; 9 | } 10 | 11 | #test-elements { 12 | .box { 13 | position: absolute; 14 | left: 100px; 15 | top: 100px; 16 | width: 100px; 17 | height: 100px; 18 | border: 0px solid #000; 19 | } 20 | } 21 | --------------------------------------------------------------------------------