Repository: wilsonpage/fastdom
Branch: master
Commit: 01524d7b9078
Files: 23
Total size: 76.2 KB
Directory structure:
gitextract_gaqz58xe/
├── .gitignore
├── .jshintignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── README.md
├── bower.json
├── examples/
│ ├── animation.html
│ └── aspect-ratio.html
├── extensions/
│ ├── fastdom-promised.d.ts
│ ├── fastdom-promised.js
│ └── fastdom-sandbox.js
├── fastdom-strict.js
├── fastdom.d.ts
├── fastdom.js
├── package.json
├── src/
│ └── fastdom-strict.js
├── test/
│ ├── fastdom-promised-test.js
│ ├── fastdom-sandbox-test.js
│ ├── fastdom-strict-test.js
│ ├── fastdom-test.js
│ └── karma.conf.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
npm-debug.log
test/coverage
================================================
FILE: .jshintignore
================================================
node_modules
fastdom.js
strict.js
================================================
FILE: .jshintrc
================================================
{
"camelcase": false,
"forin": false,
"latedef": "nofunc",
"newcap": false,
"noarg": true,
"node": true,
"nonew": true,
"quotmark": "single",
"undef": true,
"unused": "vars",
"trailing": true,
"maxlen": 80,
"laxbreak": true,
"sub": true,
"eqnull": true,
"expr": true,
"maxerr": 1000,
"regexdash": true,
"laxcomma": true,
"proto": true,
"boss": true,
"esnext": true,
"browser": true,
"devel": true,
"nonstandard": true,
"worker": true,
"-W078": true,
"predef": [
"define"
]
}
================================================
FILE: .npmignore
================================================
**/*.
bower.json
webpack.config.js
node_modules
examples
test
================================================
FILE: .travis.yml
================================================
sudo: required
dist: trusty
language: node_js
node_js:
- 5.1.0
addons:
firefox: latest
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
before_script:
- export CHROME_BIN=$(which google-chrome-stable)
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
after_script:
- npm run coveralls
================================================
FILE: README.md
================================================
# fastdom [](https://twitter.com/wilsonpage)
[](https://travis-ci.org/wilsonpage/fastdom) [](http://badge.fury.io/js/fastdom) []() [](https://coveralls.io/github/wilsonpage/fastdom?branch=master) 
Eliminates layout thrashing by batching DOM read/write operations (~600 bytes minified gzipped).
```js
fastdom.measure(() => {
console.log('measure');
});
fastdom.mutate(() => {
console.log('mutate');
});
fastdom.measure(() => {
console.log('measure');
});
fastdom.mutate(() => {
console.log('mutate');
});
```
Outputs:
```
measure
measure
mutate
mutate
```
## Examples
- [Animation example](http://wilsonpage.github.io/fastdom/examples/animation.html)
- [Aspect ratio example](http://wilsonpage.github.io/fastdom/examples/aspect-ratio.html)
## Installation
FastDom is CommonJS and AMD compatible, you can install it in one of the following ways:
```sh
$ npm install fastdom --save
```
or [download](http://github.com/wilsonpage/fastdom/raw/master/fastdom.js).
## How it works
FastDom works as a regulatory layer between your app/library and the DOM. By batching DOM access we **avoid unnecessary document reflows** and dramatically **speed up layout performance**.
Each measure/mutate job is added to a corresponding measure/mutate queue. The queues are emptied (reads, then writes) at the turn of the next frame using [`window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame).
FastDom aims to behave like a singleton across *all* modules in your app. When any module requires `'fastdom'` they get the same instance back, meaning FastDom can harmonize DOM access app-wide.
Potentially a third-party library could depend on FastDom, and better integrate within an app that itself uses it.
## API
### FastDom#measure(callback[, context])
Schedules a job for the 'measure' queue. Returns a unique ID that can be used to clear the scheduled job.
```js
fastdom.measure(() => {
const width = element.clientWidth;
});
```
### FastDom#mutate(callback[, context])
Schedules a job for the 'mutate' queue. Returns a unique ID that can be used to clear the scheduled job.
```js
fastdom.mutate(() => {
element.style.width = width + 'px';
});
```
### FastDom#clear(id)
Clears **any** scheduled job.
```js
const read = fastdom.measure(() => {});
const write = fastdom.mutate(() => {});
fastdom.clear(read);
fastdom.clear(write);
```
## Strict mode
It's very important that all DOM mutations or measurements go through `fastdom` to ensure good performance; to help you with this we wrote `fastdom-strict`. When `fastdom-strict.js` is loaded, it will throw errors when sensitive DOM APIs are called at the wrong time.
This is useful when working with a large team who might not all be aware of `fastdom` or its benefits. It can also prove useful for catching 'un-fastdom-ed' code when migrating an app to `fastdom`.
```html
<script src="fastdom.js"></script>
<script src="fastdom-strict.js"></script>
```
```js
element.clientWidth; // throws
fastdom.mutate(function() { element.clientWidth; }); // throws
fastdom.measure(function() { element.clientWidth; }); // does not throw
```
```js
"Error: Can only get .clientWidth during 'measure' phase"
```
- `fastdom-strict` will not throw if nodes are not attached to the document.
- You should use `fastdom-strict` in development to catch rendering performance issues before they hit production.
- It is not advisable to use `fastdom-strict` in production.
## Exceptions
FastDom is async, this can therefore mean that when a job comes around to being executed, the node you were working with may no longer be there. These errors are usually not critical, but they can cripple your app.
FastDom allows you to register a `catch` handler. If `fastdom.catch` has been registered, FastDom will catch any errors that occur in your jobs, and run the handler instead.
```js
fastdom.catch = (error) => {
// Do something if you want
};
```
## Extensions
The core `fastdom` library is designed to be as light as possible. Additional functionality can be bolted on in the form of 'extensions'. It's worth noting that `fastdom` is a 'singleton' by design, so all tasks (even those scheduled by extensions) will reach the same global task queue.
**Fastdom ships with some extensions:**
- [`fastdom-promised`](extensions/fastdom-promised.js) - Adds Promise based API
- [`fastdom-sandbox`](extensions/fastdom-sandbox.js) - Adds task grouping concepts
### Using an extension
Use the `.extend()` method to extend the current `fastdom` to create a new object.
```html
<script src="fastdom.js"></script>
<script src="extensions/fastdom-promised.js"></script>
```
```js
// extend fastdom
const myFastdom = fastdom.extend(fastdomPromised);
// use new api
myFastdom.mutate(...).then(...);
```
Extensions can be chained to construct a fully customised `fastdom`.
```js
const myFastdom = fastdom
.extend(fastdomPromised)
.extend(fastdomSandbox);
```
### Writing an extension
```js
const myFastdom = fastdom.extend({
measure(fn, ctx) {
// do custom stuff ...
// then call the parent method
return this.fastdom.measure(fn, ctx);
},
mutate: ...
});
```
You'll notice `this.fastdom` references the parent `fastdom`. If you're extending a core API and aren't calling the parent method, you're doing something wrong.
When distributing an extension only export a plain object to allow users to compose their own `fastdom`.
```js
module.exports = {
measure: ...,
mutate: ...,
clear: ...
};
```
## Tests
```sh
$ npm install
$ npm test
```
## Author
- **Wilson Page** - [@wilsonpage](http://twitter.com/wilsonpage)
## Contributors
- **Wilson Page** - [@wilsonpage](http://twitter.com/wilsonpage)
- **Paul Irish** - [@paulirish](http://github.com/paulirish)
- **Kornel Lesinski** - [@pornel](http://github.com/pornel)
- **George Crawford** - [@georgecrawford](http://github.com/georgecrawford)
## License
(The MIT License)
Copyright (c) 2016 Wilson Page <wilsonpage@me.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: bower.json
================================================
{
"name": "fastdom",
"description": "Eliminates layout thrashing by batching DOM read/write operations",
"main": "fastdom.js",
"scripts": [
"fastdom.js"
],
"ignore": [
"**/*.",
"webpack.config.js",
"examples/",
"test/",
"README.md"
],
"license": "MIT",
"_source": "git@github.com/wilsonpage/fastdom.git"
}
================================================
FILE: examples/animation.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>FastDom: Animation Example</title>
<script type="text/javascript" src="../fastdom.min.js"></script>
</head>
<body>
<style>
.mover {
/* Stolen shamelessly from https://developers.google.com/chrome-developer-tools/docs/demos/too-much-layout/ */
background: url("data:image/png;base64,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");
height: 100px;
width: 100px;
position: absolute;
/*20% fps boost as described here https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas/ */
will-change: transform;
}
button.active {
color: red;
}
.intro {
margin: 10px;
background: rgb(214, 213, 213);
border: 1px solid grey;
padding: 10px;
}
</style>
<div class="intro">An adaptation of the demo from the Google Developers article <a href="https://developers.google.com/chrome-developer-tools/docs/demos/too-much-layout/">Diagnosing forced synchronous layouts</a>. In the article, forced synchronous layouts are fixed by simply <em>not doing any reads at all</em>. Rather than this extreme solution, which will not be appropriate for many use cases, we instead use <strong>fastdom</strong> to intelligently defer and batch the DOM reads and writes, and get similar performance gains as a result.</div>
<label>Number of elements <input id="count" type="text" value="400" /></label>
<button id="sync" class="active">Forced synchronous layout</button>
<button id="async">Run with FastDom</button>
<button id="noread">No DOM reads</button>
<button id="toggle">Start</button>
<div id='test'></div>
<script>
var moveMethod = 'sync',
count = document.getElementById('count'),
test = document.getElementById('test'),
timestamp, raf, movers;
var mover = {
sync: function(m) {
// Read the top offset, and use that for the left position
mover.setLeft(movers[m], movers[m].offsetTop);
},
async: function(m) {
// Use fastdom to batch the reads
// and writes with exactly the same
// code as the 'sync' routine
fastdom.measure(function() {
var top = movers[m].offsetTop;
fastdom.mutate(function() {
mover.setLeft(movers[m], top);
});
});
},
noread: function(m) {
// Simply use the array index
// as the top value, so no DOM
// read is required
mover.setLeft(movers[m], m);
},
setLeft: function(mover, top) {
mover.style.transform = 'translateX( ' +((Math.sin(top + timestamp/1000) + 1) * 500) + 'px)';
}
};
function update(thisTimestamp) {
timestamp = thisTimestamp;
for (var m = 0; m < movers.length; m++) {
mover[moveMethod](m);
}
raf = window.requestAnimationFrame(update);
}
function toggleAnim(e) {
var html, num;
if (raf) {
window.cancelAnimationFrame(raf);
raf = false;
e.currentTarget.innerHTML = 'Start';
count.disabled = false;
} else {
html = '';
num = count.value;
for (i = 0; i < num; i++) {
html += '<div class="mover"></div>';
}
test.innerHTML = html;
movers = test.querySelectorAll('.mover');
movers[0].style.top = '150px';
for (var m = 1; m < movers.length; m++) {
movers[m].style.top = (m * 20) + 150 + 'px';
}
raf = window.requestAnimationFrame(update);
e.currentTarget.innerHTML = 'Stop';
count.disabled = true;
}
}
function setMethod(method) {
document.getElementById(moveMethod).classList.remove('active');
document.getElementById(method).classList.add('active');
moveMethod = method;
}
document.getElementById('toggle').addEventListener('click', toggleAnim);
document.getElementById('sync').addEventListener('click', function() {
setMethod('sync');
});
document.getElementById('async').addEventListener('click', function() {
setMethod('async');
});
document.getElementById('noread').addEventListener('click', function() {
setMethod('noread');
});
</script>
</body>
</html>
================================================
FILE: examples/aspect-ratio.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>FastDom: Aspect Ratio Example</title>
<style>
* {
box-sizing: border-box;
}
div {
float: left;
background: silver;
border: solid 2px white;
}
</style>
</head>
<body>
<label>Number of elements <input id="input" type="text" value="100" /></label>
<button id="withoutFastDom">Run without FastDom</button>
<button id="withRequestAnimationFrame">Run with requestAnimationFrame</button>
<button id="withFastDom">Run with FastDom</button>
<button id="resetbtn">reset</button>
<section id="perf"></section>
<section id="container"></section>
<script src="../fastdom.js"></script>
<script>
var n;
var start;
var divs;
// Setup
function reset(done) {
n = input.value;
divs = [];
fastdom.measure(function() {
var winWidth = window.innerWidth;
fastdom.mutate(function() {
container.innerHTML = '';
for (var i = 0; i < n; i++) {
var div = document.createElement('div');
div.style.width = Math.round(Math.random() * winWidth) + 'px';
container.appendChild(div);
divs.push(div);
}
if (done) done();
});
});
}
function setAspect(div, i) {
var aspect = 9 / 16;
var isLast = i === (n - 1);
var h = div.clientWidth * aspect;
div.style.height = h + 'px';
if (isLast) {
displayPerf(performance.now() - start);
}
}
function setAspectRequestAnimationFrame(div, i) {
var aspect = 9 / 16;
var isLast = i === (n - 1);
// READ
requestAnimationFrame(function() {
var h = div.clientWidth * aspect;
// WRITE
requestAnimationFrame(function() {
div.style.height = h + 'px';
if (isLast) {
displayPerf(performance.now() - start);
}
});
});
}
function setAspectFastDom(div, i) {
var aspect = 9 / 16;
var isLast = i === (n - 1);
// READ
fastdom.measure(function() {
var h = div.clientWidth * aspect;
// WRITE
fastdom.mutate(function() {
div.style.height = h + 'px';
if (isLast) {
displayPerf(performance.now() - start);
}
});
});
}
function displayPerf(ms) {
perf.textContent = ms + 'ms';
}
withoutFastDom.onclick = function() {
reset(function() {
start = performance.now();
divs.forEach(setAspect);
});
};
withFastDom.onclick = function() {
reset(function() {
start = performance.now();
divs.forEach(setAspectFastDom);
});
};
withRequestAnimationFrame.onclick = function() {
reset(function() {
start = performance.now();
divs.forEach(setAspectRequestAnimationFrame);
});
};
resetbtn.onclick = function() {
reset();
};
</script>
</body>
</html>
================================================
FILE: extensions/fastdom-promised.d.ts
================================================
declare namespace FastdomPromised {
export function clear<T extends Promise<any>>(task: T): void;
export function initialize(): void;
export function measure<T extends () => void>(task: T, context?: any): Promise<ReturnType<T>>;
export function mutate<T extends () => void>(task: T, context?: any): Promise<ReturnType<T>>;
}
export = FastdomPromised;
================================================
FILE: extensions/fastdom-promised.js
================================================
!(function() {
/**
* Wraps fastdom in a Promise API
* for improved control-flow.
*
* @example
*
* // returning a result
* fastdom.measure(() => el.clientWidth)
* .then(result => ...);
*
* // returning promises from tasks
* fastdom.measure(() => {
* var w = el1.clientWidth;
* return fastdom.mutate(() => el2.style.width = w + 'px');
* }).then(() => console.log('all done'));
*
* // clearing pending tasks
* var promise = fastdom.measure(...)
* fastdom.clear(promise);
*
* @type {Object}
*/
var exports = {
initialize: function() {
this._tasks = new Map();
},
mutate: function(fn, ctx) {
return create(this, 'mutate', fn, ctx);
},
measure: function(fn, ctx) {
return create(this, 'measure', fn, ctx);
},
clear: function(promise) {
var tasks = this._tasks;
var task = tasks.get(promise);
this.fastdom.clear(task);
tasks.delete(promise);
}
};
/**
* Create a fastdom task wrapped in
* a 'cancellable' Promise.
*
* @param {FastDom} fastdom
* @param {String} type - 'measure'|'mutate'
* @param {Function} fn
* @return {Promise}
*/
function create(promised, type, fn, ctx) {
var tasks = promised._tasks;
var fastdom = promised.fastdom;
var task;
var promise = new Promise(function(resolve, reject) {
task = fastdom[type](function() {
tasks.delete(promise);
try { resolve(ctx ? fn.call(ctx) : fn()); }
catch (e) { reject(e); }
}, ctx);
});
tasks.set(promise, task);
return promise;
}
// Expose to CJS, AMD or global
if ((typeof define)[0] == 'f') define(function() { return exports; });
else if ((typeof module)[0] == 'o') module.exports = exports;
else window.fastdomPromised = exports;
})();
================================================
FILE: extensions/fastdom-sandbox.js
================================================
(function(exports) {
/**
* Mini logger
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console, '[fastdom-sandbox]') : function() {};
/**
* Exports
*/
/**
* Create a new `Sandbox`.
*
* Scheduling tasks via a sandbox is
* useful because you can clear all
* sandboxed tasks in one go.
*
* This is handy when working with view
* components. You can create one sandbox
* per component and call `.clear()` when
* tearing down.
*
* @example
*
* var sandbox = fastdom.sandbox();
*
* sandbox.measure(function() { console.log(1); });
* sandbox.measure(function() { console.log(2); });
*
* fastdom.measure(function() { console.log(3); });
* fastdom.measure(function() { console.log(4); });
*
* sandbox.clear();
*
* // => 3
* // => 4
*
* @return {Sandbox}
* @public
*/
exports.sandbox = function() {
return new Sandbox(this.fastdom);
};
/**
* Initialize a new `Sandbox`
*
* @param {FastDom} fastdom
*/
function Sandbox(fastdom) {
this.fastdom = fastdom;
this.tasks = [];
debug('initialized');
}
/**
* Schedule a 'measure' task.
*
* @param {Function} fn
* @param {Object} ctx
* @return {Object} can be passed to .clear()
*/
Sandbox.prototype.measure = function(fn, ctx) {
var tasks = this.tasks;
var task = this.fastdom.measure(function() {
tasks.splice(tasks.indexOf(task));
return fn.call(ctx);
});
tasks.push(task);
return task;
};
/**
* Schedule a 'mutate' task.
*
* @param {Function} fn
* @param {Object} ctx
* @return {Object} can be passed to .clear()
*/
Sandbox.prototype.mutate = function(fn, ctx) {
var tasks = this.tasks;
var task = this.fastdom.mutate(function() {
tasks.splice(tasks.indexOf(task));
return fn.call(ctx);
});
this.tasks.push(task);
return task;
};
/**
* Clear a single task or is no task is
* passsed, all tasks in the `Sandbox`.
*
* @param {Object} task (optional)
*/
Sandbox.prototype.clear = function(task) {
if (!arguments.length) clearAll(this.fastdom, this.tasks);
remove(this.tasks, task);
return this.fastdom.clear(task);
};
/**
* Clears all the given tasks from
* the given `FastDom`.
*
* @param {FastDom} fastdom
* @param {Array} tasks
* @private
*/
function clearAll(fastdom, tasks) {
debug('clear all', fastdom, tasks);
var i = tasks.length;
while (i--) {
fastdom.clear(tasks[i]);
tasks.splice(i, 1);
}
}
/**
* Remove an item from an Array.
*
* @param {Array} array
* @param {*} item
* @return {Boolean}
*/
function remove(array, item) {
var index = array.indexOf(item);
return !!~index && !!array.splice(index, 1);
}
/**
* Expose
*/
if ((typeof define)[0] == 'f') define(function() { return exports; });
else if ((typeof module)[0] == 'o') module.exports = exports;
else window.fastdomSandbox = exports;
})({});
================================================
FILE: fastdom-strict.js
================================================
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("fastdom"));
else if(typeof define === 'function' && define.amd)
define(["fastdom"], factory);
else if(typeof exports === 'object')
exports["fastdom"] = factory(require("fastdom"));
else
root["fastdom"] = factory(root["fastdom"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE_2__) {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
'use strict';
var strictdom = __webpack_require__(1);
var fastdom = __webpack_require__(2);
/**
* Mini logger
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console, '[fastdom-strict]') : function() {};
/**
* Enabled state
*
* @type {Boolean}
*/
var enabled = false;
window.fastdom = module.exports = fastdom.extend({
measure: function(fn, ctx) {
debug('measure');
var task = !ctx ? fn : fn.bind(ctx);
return this.fastdom.measure(function() {
if (!enabled) return task();
return strictdom.phase('measure', task);
}, ctx);
},
mutate: function(fn, ctx) {
debug('mutate');
var task = !ctx ? fn : fn.bind(ctx);
return this.fastdom.mutate(function() {
if (!enabled) return task();
return strictdom.phase('mutate', task);
}, ctx);
},
strict: function(value) {
if (value) {
enabled = true;
strictdom.enable();
} else {
enabled = false;
strictdom.disable();
}
}
});
// turn on strict-mode
window.fastdom.strict(true);
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_RESULT__;!(function() {
'use strict';
var debug = 0 ? console.log.bind(console, '[strictdom]') : function() {};
/**
* Crude webkit test.
*
* @type {Boolean}
*/
// var isWebkit = !!window.webkitURL;
/**
* List of properties observed.
*
* @type {Object}
*/
var properties = {
prototype: {
Document: {
execCommand: Mutate,
elementFromPoint: Measure,
elementsFromPoint: Measure,
scrollingElement: Measure
},
Node: {
appendChild: {
type: Mutate,
test: function(dom, parent, args) {
var attached = isAttached(parent) || isAttached(args[0]);
if (attached && dom.not('mutate')) throw error(3, this.name);
}
},
insertBefore: {
type: Mutate,
test: function(dom, parent, args) {
var attached = isAttached(parent) || isAttached(args[0]);
if (attached && dom.not('mutate')) throw error(3, this.name);
}
},
removeChild: {
type: Mutate,
test: function(dom, parent, args) {
var attached = isAttached(parent) || isAttached(args[0]);
if (attached && dom.not('mutate')) throw error(3, this.name);
}
},
textContent: Mutate
},
Element: {
scrollIntoView: Mutate,
scrollBy: Mutate,
scrollTo: Mutate,
getClientRects: Measure,
getBoundingClientRect: Measure,
clientLeft: Measure,
clientWidth: Measure,
clientHeight: Measure,
scrollLeft: Accessor,
scrollTop: Accessor,
scrollWidth: Measure,
scrollHeight: Measure,
innerHTML: Mutate,
outerHTML: Mutate,
insertAdjacentHTML: Mutate,
remove: Mutate,
setAttribute: Mutate,
removeAttribute: Mutate,
className: Mutate,
classList: ClassList
},
HTMLElement: {
offsetLeft: Measure,
offsetTop: Measure,
offsetWidth: Measure,
offsetHeight: Measure,
offsetParent: Measure,
innerText: Accessor,
outerText: Accessor,
focus: Measure,
blur: Measure,
style: Style,
// `element.dataset` is hard to wrap.
// We could use `Proxy` but it's not
// supported in Chrome yet. Not too
// concerned as `data-` attributes are
// not often associated with render.
// dataset: DATASET
},
CharacterData: {
remove: Mutate,
data: Mutate
},
Range: {
getClientRects: Measure,
getBoundingClientRect: Measure
},
MouseEvent: {
layerX: Measure,
layerY: Measure,
offsetX: Measure,
offsetY: Measure
},
HTMLButtonElement: {
reportValidity: Measure
},
HTMLDialogElement: {
showModal: Mutate
},
HTMLFieldSetElement: {
reportValidity: Measure
},
HTMLImageElement: {
width: Accessor,
height: Accessor,
x: Measure,
y: Measure
},
HTMLInputElement: {
reportValidity: Measure
},
HTMLKeygenElement: {
reportValidity: Measure
},
SVGSVGElement: {
currentScale: Accessor
}
},
instance: {
window: {
getComputedStyle: {
type: Measure,
/**
* Throws when the Element is in attached
* and strictdom is not in the 'measure' phase.
*
* @param {StrictDom} strictdom
* @param {Window} win
* @param {Object} args
*/
test: function(strictdom, win, args) {
if (isAttached(args[0]) && strictdom.not('measure')) {
throw error(2, 'getComputedStyle');
}
}
},
// innerWidth: {
// type: isWebkit ? Value : Measure,
//
// /**
// * Throws when the window is nested (in <iframe>)
// * and StrictDom is not in the 'measure' phase.
// *
// * @param {StrictDom} strictdom
// */
// test: function(strictdom) {
// var inIframe = window !== window.top;
// if (inIframe && strictdom.not('measure')) {
// throw error(2, '`.innerWidth` (in iframe)');
// }
// }
// },
//
// innerHeight: {
// type: isWebkit ? Value : Measure,
//
// /**
// * Throws when the window is nested (in <iframe>)
// * and StrictDom is not in the 'measure' phase.
// *
// * @param {StrictDom} strictdom
// */
// test: function(strictdom) {
// var inIframe = window !== window.top;
// if (inIframe && strictdom.not('measure')) {
// throw error(2, '`.innerHeight` (in iframe)');
// }
// }
// },
//
// scrollX: isWebkit ? Value : Measure,
// scrollY: isWebkit ? Value : Measure,
scrollBy: Mutate,
scrollTo: Mutate,
scroll: Mutate,
}
}
};
/**
* The master controller for all properties.
*
* @param {Window} win
*/
function StrictDom(win) {
this.properties = [];
this._phase = null;
this.win = win;
this.createPrototypeProperties();
this.createInstanceProperties();
}
StrictDom.prototype = {
/**
* Set the current phase.
* @param {[type]} value [description]
* @return {[type]} [description]
*/
phase: function(type, task) {
if (!arguments.length) return this._phase;
if (!this.knownPhase(type)) throw error(4, type);
var previous = this._phase;
this._phase = type;
if (typeof task != 'function') return;
var result = task();
this._phase = previous;
return result;
},
knownPhase: function(value) {
return !!~['measure', 'mutate', null].indexOf(value);
},
is: function(value) {
return this._phase === value;
},
not: function(value) {
return !this.is(value);
},
/**
* Enable strict mode.
*
* @public
*/
enable: function() {
if (this.enabled) return;
debug('enable');
var i = this.properties.length;
while (i--) this.properties[i].enable();
this.enabled = true;
},
/**
* Disable strict mode.
*
* @public
*/
disable: function() {
if (!this.enabled) return;
debug('disable');
var i = this.properties.length;
while (i--) this.properties[i].disable();
this.enabled = false;
this.phase(null);
},
/**
* Create wrappers for each of
* of the prototype properties.
*
* @private
*/
createPrototypeProperties: function() {
debug('create prototype properties');
var props = properties.prototype;
for (var key in props) {
for (var name in props[key]) {
var object = this.win[key] && this.win[key].prototype;
if (!object || !object.hasOwnProperty(name)) continue;
this.properties.push(this.create(object, name, props[key][name]));
}
}
},
/**
* Create wrappers for each of
* of the instance properties.
*
* @private
*/
createInstanceProperties: function() {
debug('create instance properties');
var props = properties.instance;
for (var key in props) {
for (var name in props[key]) {
var object = this.win[key];
if (!object || !object.hasOwnProperty(name)) continue;
this.properties.push(this.create(object, name, props[key][name]));
}
}
},
/**
* Create a wrapped `Property` that
* can be individually enabled/disabled.
*
* @param {Object} object - the parent object (eg. Node.prototype)
* @param {String} name - the property name (eg. 'appendChild')
* @param {(constructor|Object)} config - from the above property definition
* @return {Property}
*/
create: function(object, name, config) {
debug('create', name);
var Constructor = config.type || config;
return new Constructor(object, name, config, this);
}
};
/**
* Create a new `Property`.
*
* A wrapper around a property that observes
* usage, throwing errors when used in the
* incorrect phase.
*
* @param {Object} object - the parent object (eg. Node.prototype)
* @param {[type]} name - the property name (eg. 'appendChild')
* @param {(constructor|Object)} config - from the above definition
* @param {StrictDom} strictdom - injected as a dependency
*/
function Property(object, name, config, strictdom) {
debug('Property', name, config);
this.strictdom = strictdom;
this.object = object;
this.name = name;
var descriptor = this.getDescriptor();
// defaults can be overriden from config
if (typeof config == 'object') Object.assign(this, config);
this.descriptors = {
unwrapped: descriptor,
wrapped: this.wrap(descriptor)
};
}
Property.prototype = {
/**
* Get the property's descriptor.
*
* @return {Object}
* @private
*/
getDescriptor: function() {
debug('get descriptor', this.name);
return Object.getOwnPropertyDescriptor(this.object, this.name);
},
/**
* Enable observation by replacing the
* current descriptor with the wrapped one.
*
* @private
*/
enable: function() {
debug('enable', this.name);
Object.defineProperty(this.object, this.name, this.descriptors.wrapped);
},
/**
* Disable observation by replacing the
* current descriptor with the original one.
*
* @private
*/
disable: function() {
debug('disable', this.name);
Object.defineProperty(this.object, this.name, this.descriptors.unwrapped);
},
// to be overwritten by subclass
wrap: function() {}
};
/**
* A wrapper for properties that measure
* geometry data from the DOM.
*
* Once a `Measure` property is enabled
* it can only be used when StrictDom
* is in the 'measure' phase, else it
* will throw.
*
* @constructor
* @extends Property
*/
function Measure() {
Property.apply(this, arguments);
}
Measure.prototype = extend(Property, {
/**
* Return a wrapped descriptor.
*
* @param {Object} descriptor
* @return {Object}
*/
wrap: function(descriptor) {
debug('wrap measure', this.name);
var clone = Object.assign({}, descriptor);
var value = descriptor.value;
var get = descriptor.get;
var self = this;
if (typeof value == 'function') {
clone.value = function() {
debug('measure', self.name);
self.test(self.strictdom, this, arguments);
return value.apply(this, arguments);
};
} else if (get) {
clone.get = function() {
debug('measure', self.name);
self.test(self.strictdom, this, arguments);
return get.apply(this, arguments);
};
}
return clone;
},
/**
* Throws an Error if the element is attached
* and StrictDOM is not in the 'measure' phase.
*
* If methods/properties are used without
* a context (eg. `getComputedStyle()` instead
* of `window.getComputedStyle()`) we infer
* a `window` context.
*
* @param {StrictDom} strictdom
* @param {Node} ctx
*/
test: function(strictdom, ctx) {
if (isAttached(ctx || window) && strictdom.not('measure')) {
throw error(2, this.name);
}
}
});
/**
* A wrapper for properties that mutate
* to the DOM, triggering style/reflow
* operations.
*
* Once a `Mutate` property is enabled
* it can only be used when StrictDom
* is in the 'measure' phase, else it
* will throw.
*
* @constructor
* @extends Property
*/
function Mutate() {
Property.apply(this, arguments);
}
Mutate.prototype = extend(Property, {
/**
* Return a wrapped descriptor.
*
* @param {Object} descriptor
* @return {Object}
*/
wrap: function(descriptor) {
debug('wrap mutate', this.name);
var clone = Object.assign({}, descriptor);
var value = descriptor.value;
var self = this;
if (typeof value == 'function') {
clone.value = function() {
self.test(self.strictdom, this, arguments);
return value.apply(this, arguments);
};
} else if (descriptor.set) {
clone.set = function() {
self.test(self.strictdom, this, arguments);
return descriptor.set.apply(this, arguments);
};
}
return clone;
},
/**
* Throws an Error if the element is attached
* and StrictDOM is not in the 'mutate' phase.
*
* If methods/properties are used without
* a context (eg. `getComputedStyle()` instead
* of `window.getComputedStyle()`) we infer
* a `window` context.
*
* @param {StrictDom} strictdom
* @param {Node} ctx
*/
test: function(strictdom, ctx) {
if (isAttached(ctx || window) && strictdom.not('mutate')) {
throw error(3, this.name);
}
}
});
/**
* A wrapper for 'accessor' (get/set) properties.
*
* An `Accessor` should be used to wrap
* properties that can both measure and mutate
* the DOM (eg. `element.scrollTop`).
*
* @constructor
* @extends Property
*/
function Accessor() {
Property.apply(this, arguments);
}
Accessor.prototype = extend(Property, {
/**
* Return a wrapped descriptor.
*
* @param {Object} descriptor
* @return {Object}
*/
wrap: function(descriptor) {
debug('wrap accessor', this.name);
var clone = Object.assign({}, descriptor);
var get = descriptor.get;
var set = descriptor.set;
var self = this;
if (get) {
clone.get = function() {
self.testRead(self.strictdom, this, arguments);
return get.apply(this, arguments);
};
}
if (descriptor.set) {
clone.set = function() {
self.testWrite(self.strictdom, this, arguments);
return set.apply(this, arguments);
};
}
return clone;
},
testRead: Measure.prototype.test,
testWrite: Mutate.prototype.test
});
/**
* A wrapper for 'value' properties.
*
* A `Value` should be used to wrap special
* values that like `window.innerWidth`, which
* in Chrome (not Gecko) are not normal 'getter'
* functions, but magical flat getters.
*
* Value wrappers are a for very special cases.
*
* @constructor
* @extends Property
*/
function Value() {
Property.apply(this, arguments);
}
Value.prototype = extend(Property, {
/**
* Calling `Object.getOwnDescriptor()` can
* trigger a reflow as it returns the `value`
* of the property. So here we just
* return an empty object instead.
*
* @return {Object}
* @private
*/
getDescriptor: function() {
return {};
},
/**
* Value wrappers are disabled by simply
* deleting them from the instance,
* revealing the original descriptor.
*
* @private
*/
disable: function() {
delete this.object[this.name];
},
/**
* Return a wrapped descriptor.
*
* `Value` properties are actually on the
* instance of objects. To wrap them we need
* to replace them with a getter which
* deletes itself on access, call into the v8
* interceptor, and then add themselves back.
*
* This won't be fast, but these are rarely
* accessed so it should be fine.
*
* @param {Object} descriptor
* @return {Object}
*/
wrap: function(descriptor) {
debug('wrap value');
var name = this.name;
var self = this;
descriptor.get = function() {
debug('get value', name);
self.test(self.strictdom, this, arguments);
self.disable();
var result = this[name];
self.enable();
return result;
};
return descriptor;
},
test: Measure.prototype.test
});
function Style() {
Property.apply(this, arguments);
}
Style.prototype = extend(Property, {
wrap: function(descriptor) {
debug('wrap style');
var strictdom = this.strictdom;
var clone = Object.assign({}, descriptor);
clone.get = function() { return new StrictStyle(this, strictdom); };
return clone;
}
});
function ClassList() {
Property.apply(this, arguments);
}
ClassList.prototype = extend(Property, {
wrap: function(descriptor) {
debug('wrap style');
var strictdom = this.strictdom;
var clone = Object.assign({}, descriptor);
clone.get = function() { return new StrictClassList(this, strictdom); };
return clone;
}
});
function StrictStyle(el, strictdom) {
this.strictdom = strictdom;
this.el = el;
}
StrictStyle.prototype = {
_getter: getDescriptor(HTMLElement.prototype, 'style').get,
_get: function() {
return this._getter.call(this.el);
},
setProperty: function(key, value) {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'style.' + key);
return this._get()[key] = value;
},
removeProperty: function(key) {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'style.' + key);
return this._get().removeProperty(key);
}
};
// dynamically construct prototype
// from real element.style
(function() {
var styles = document.createElement('div').style;
var proto = {};
for (var key in styles) {
if (styles[key] === '') {
Object.defineProperty(StrictStyle.prototype, key, {
get: getter(key),
set: setter(key)
});
}
}
[
'item',
'getPropertyValue',
'getPropertyCSSValue',
'getPropertyPriority'
].forEach(function(method) {
StrictStyle.prototype[method] = caller(method);
});
function getter(key) {
return function() {
return this._get()[key];
};
}
function setter(key) {
return function(value) {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'style.' + key);
return this.setProperty(key, value);
};
}
function caller(key) {
return function() {
var style = this._get();
return style[key].apply(style, arguments);
};
}
return proto;
})();
function StrictClassList(el, strictdom) {
this.strictdom = strictdom;
this.el = el;
}
StrictClassList.prototype = {
_getter: getDescriptor(Element.prototype, 'classList').get,
_get: function() { return this._getter.call(this.el); },
add: function(className) {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'class names');
this._get().add(className);
},
contains: function(className) {
return this._get().contains(className);
},
remove: function(className) {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'class names');
this._get().remove(className);
},
toggle: function() {
var illegal = isAttached(this.el) && this.strictdom.not('mutate');
if (illegal) throw error(1, 'class names');
var classList = this._get();
return classList.toggle.apply(classList, arguments);
}
};
/**
* Utils
*/
function error(type) {
return new Error({
1: 'Can only set ' + arguments[1] + ' during \'mutate\' phase',
2: 'Can only get ' + arguments[1] + ' during \'measure\' phase',
3: 'Can only call `.' + arguments[1] + '()` during \'mutate\' phase',
4: 'Invalid phase: ' + arguments[1]
}[type]);
}
function getDescriptor(object, prop) {
return Object.getOwnPropertyDescriptor(object, prop);
}
function extend(parent, props) {
return Object.assign(Object.create(parent.prototype), props);
}
function isAttached(el) {
return el === window || document.contains(el);
}
/**
* Exports
*/
// Only ever allow one `StrictDom` per document
var exports = window['strictdom'] = (window['strictdom'] || new StrictDom(window)); // jshint ignore:line
// CJS & AMD support
if (("function")[0] == 'f') !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { return exports; }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
else if ((typeof module)[0] == 'o') module.exports = exports;
})();
/***/ }),
/* 2 */
/***/ (function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
/***/ })
/******/ ])
});
;
================================================
FILE: fastdom.d.ts
================================================
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
declare class Fastdom {
clear<T extends () => void>(task: T): boolean;
extend<T extends object>(props: T): Omit<this, keyof T & keyof this> & T;
measure<T extends () => void>(task: T, context?: any): T;
mutate<T extends () => void>(task: T, context?: any): T;
catch: null | ((e: unknown) => any);
}
declare const fastdom: Fastdom
export default fastdom;
================================================
FILE: fastdom.js
================================================
!(function(win) {
/**
* FastDom
*
* Eliminates layout thrashing
* by batching DOM read/write
* interactions.
*
* @author Wilson Page <wilsonpage@me.com>
* @author Kornel Lesinski <kornel.lesinski@ft.com>
*/
'use strict';
/**
* Mini logger
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console, '[fastdom]') : function() {};
/**
* Normalized rAF
*
* @type {Function}
*/
var raf = win.requestAnimationFrame
|| win.webkitRequestAnimationFrame
|| win.mozRequestAnimationFrame
|| win.msRequestAnimationFrame
|| function(cb) { return setTimeout(cb, 16); };
/**
* Initialize a `FastDom`.
*
* @constructor
*/
function FastDom() {
var self = this;
self.reads = [];
self.writes = [];
self.raf = raf.bind(win); // test hook
debug('initialized', self);
}
FastDom.prototype = {
constructor: FastDom,
/**
* We run this inside a try catch
* so that if any jobs error, we
* are able to recover and continue
* to flush the batch until it's empty.
*
* @param {Array} tasks
*/
runTasks: function(tasks) {
debug('run tasks');
var task; while (task = tasks.shift()) task();
},
/**
* Adds a job to the read batch and
* schedules a new frame if need be.
*
* @param {Function} fn
* @param {Object} ctx the context to be bound to `fn` (optional).
* @public
*/
measure: function(fn, ctx) {
debug('measure');
var task = !ctx ? fn : fn.bind(ctx);
this.reads.push(task);
scheduleFlush(this);
return task;
},
/**
* Adds a job to the
* write batch and schedules
* a new frame if need be.
*
* @param {Function} fn
* @param {Object} ctx the context to be bound to `fn` (optional).
* @public
*/
mutate: function(fn, ctx) {
debug('mutate');
var task = !ctx ? fn : fn.bind(ctx);
this.writes.push(task);
scheduleFlush(this);
return task;
},
/**
* Clears a scheduled 'read' or 'write' task.
*
* @param {Object} task
* @return {Boolean} success
* @public
*/
clear: function(task) {
debug('clear', task);
return remove(this.reads, task) || remove(this.writes, task);
},
/**
* Extend this FastDom with some
* custom functionality.
*
* Because fastdom must *always* be a
* singleton, we're actually extending
* the fastdom instance. This means tasks
* scheduled by an extension still enter
* fastdom's global task queue.
*
* The 'super' instance can be accessed
* from `this.fastdom`.
*
* @example
*
* var myFastdom = fastdom.extend({
* initialize: function() {
* // runs on creation
* },
*
* // override a method
* measure: function(fn) {
* // do extra stuff ...
*
* // then call the original
* return this.fastdom.measure(fn);
* },
*
* ...
* });
*
* @param {Object} props properties to mixin
* @return {FastDom}
*/
extend: function(props) {
debug('extend', props);
if (typeof props != 'object') throw new Error('expected object');
var child = Object.create(this);
mixin(child, props);
child.fastdom = this;
// run optional creation hook
if (child.initialize) child.initialize();
return child;
},
// override this with a function
// to prevent Errors in console
// when tasks throw
catch: null
};
/**
* Schedules a new read/write
* batch if one isn't pending.
*
* @private
*/
function scheduleFlush(fastdom) {
if (!fastdom.scheduled) {
fastdom.scheduled = true;
fastdom.raf(flush.bind(null, fastdom));
debug('flush scheduled');
}
}
/**
* Runs queued `read` and `write` tasks.
*
* Errors are caught and thrown by default.
* If a `.catch` function has been defined
* it is called instead.
*
* @private
*/
function flush(fastdom) {
debug('flush');
var writes = fastdom.writes;
var reads = fastdom.reads;
var error;
try {
debug('flushing reads', reads.length);
fastdom.runTasks(reads);
debug('flushing writes', writes.length);
fastdom.runTasks(writes);
} catch (e) { error = e; }
fastdom.scheduled = false;
// If the batch errored we may still have tasks queued
if (reads.length || writes.length) scheduleFlush(fastdom);
if (error) {
debug('task errored', error.message);
if (fastdom.catch) fastdom.catch(error);
else throw error;
}
}
/**
* Remove an item from an Array.
*
* @param {Array} array
* @param {*} item
* @return {Boolean}
*/
function remove(array, item) {
var index = array.indexOf(item);
return !!~index && !!array.splice(index, 1);
}
/**
* Mixin own properties of source
* object into the target.
*
* @param {Object} target
* @param {Object} source
*/
function mixin(target, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) target[key] = source[key];
}
}
// There should never be more than
// one instance of `FastDom` in an app
var exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line
// Expose to CJS & AMD
if ((typeof define) == 'function') define(function() { return exports; });
else if ((typeof module) == 'object') module.exports = exports;
})( typeof window !== 'undefined' ? window : typeof this != 'undefined' ? this : globalThis);
================================================
FILE: package.json
================================================
{
"name": "fastdom",
"description": "Eliminates layout thrashing by batching DOM read/write operations",
"version": "1.0.11",
"main": "fastdom.js",
"types": "fastdom.d.ts",
"scripts": {
"lint": "jshint src/*",
"unit": "karma start test/karma.conf.js --single-run",
"test": "npm run -s unit && npm run -s lint",
"test-dev": "karma start test/karma.conf.js --browsers Firefox",
"coveralls": "cat test/coverage/lcov.info | coveralls",
"compress": "uglifyjs fastdom.js --compress='drop_console,sequences,dead_code,booleans,conditionals,unused,if_return,join_vars,pure_funcs=\"debug\"' --mangle --reserved='require,define,module,exports' > fastdom.min.js",
"build": "webpack && npm run -s compress",
"watch": "webpack -w"
},
"homepage": "https://github.com/wilsonpage/fastdom",
"author": {
"name": "Wilson Page",
"email": "wilsonpage@me.com"
},
"repository": {
"type": "git",
"url": "git://github.com/wilsonpage/fastdom.git"
},
"license": "MIT",
"devDependencies": {
"chai": "^3.4.1",
"coveralls": "^2.11.6",
"jshint": "^2.8.0",
"karma": "^0.13.15",
"karma-chai-sinon": "^0.1.5",
"karma-chrome-launcher": "^0.2.2",
"karma-coverage": "^0.5.3",
"karma-firefox-launcher": "^0.1.3",
"karma-mocha": "^0.2.1",
"karma-mocha-reporter": "^1.1.3",
"mocha": "^2.3.4",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0",
"uglify-js": "^2.4.23",
"webpack": "^1.12.9"
},
"dependencies": {
"strictdom": "^1.0.1"
}
}
================================================
FILE: src/fastdom-strict.js
================================================
'use strict';
var strictdom = require('strictdom');
var fastdom = require('../fastdom');
/**
* Mini logger
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console, '[fastdom-strict]') : function() {};
/**
* Enabled state
*
* @type {Boolean}
*/
var enabled = false;
window.fastdom = module.exports = fastdom.extend({
measure: function(fn, ctx) {
debug('measure');
var task = !ctx ? fn : fn.bind(ctx);
return this.fastdom.measure(function() {
if (!enabled) return task();
return strictdom.phase('measure', task);
}, ctx);
},
mutate: function(fn, ctx) {
debug('mutate');
var task = !ctx ? fn : fn.bind(ctx);
return this.fastdom.mutate(function() {
if (!enabled) return task();
return strictdom.phase('mutate', task);
}, ctx);
},
strict: function(value) {
if (value) {
enabled = true;
strictdom.enable();
} else {
enabled = false;
strictdom.disable();
}
}
});
// turn on strict-mode
window.fastdom.strict(true);
================================================
FILE: test/fastdom-promised-test.js
================================================
/*global suite, setup, test, assert, sinon, fastdomPromised*/
/*jshint maxlen:false*/
suite('fastdom-promised', function() {
var fastdom;
setup(function() {
fastdom = window.fastdom.extend(fastdomPromised);
});
test('it returns a Promise that resolves after the task is run', function(done) {
var spy = sinon.spy();
fastdom.measure(spy)
.then(function() {
sinon.assert.calledOnce(spy);
done();
});
});
test('promises can be returned from tasks', function() {
var spy1 = sinon.spy();
var spy2 = sinon.spy();
return fastdom.measure(function() {
spy1();
return fastdom.mutate(spy2);
})
.then(function() {
sinon.assert.calledOnce(spy1);
sinon.assert.calledOnce(spy2);
assert.isTrue(spy1.calledBefore(spy2));
});
});
test('calling `fastdom.clear(promise)` works', function(done) {
var spy = sinon.spy();
var task = fastdom.measure(spy);
fastdom.clear(task);
requestAnimationFrame(function() {
sinon.assert.notCalled(spy);
done();
});
});
test('it calls callback with given context', function() {
var spy = sinon.spy();
var ctx = {};
return fastdom.measure(spy, ctx)
.then(function() {
sinon.assert.calledOn(spy, ctx);
});
});
});
================================================
FILE: test/fastdom-sandbox-test.js
================================================
/*global suite, setup, test, assert, sinon, fastdomSandbox, fastdomPromised*/
/* jshint maxlen:false */
suite('fastdom-sandbox', function() {
var raf = window.requestAnimationFrame;
var fastdom;
setup(function() {
fastdom = new window.fastdom.constructor();
fastdom = fastdom.extend(window.fastdomSandbox);
});
test('It works as normal', function(done) {
var sandbox = fastdom.sandbox();
sandbox.measure(function() {
sandbox.mutate(function() {
done();
});
});
});
test('Its possible to clear all sandbox jobs', function(done) {
var sandbox = fastdom.sandbox();
var spy = sinon.spy();
sandbox.measure(spy);
sandbox.mutate(spy);
fastdom.measure(function() {
fastdom.mutate(function() {
assert.isTrue(spy.notCalled);
done();
});
});
sandbox.clear();
});
test('It clears individual tasks', function(done) {
var sandbox = fastdom.sandbox();
var spy = sinon.spy();
var task = sandbox.measure(spy);
sandbox.clear(task);
sandbox.measure(function() {
assert.isTrue(spy.notCalled);
done();
});
});
test('it works with fastdom-promised', function(done) {
var myFastdom = fastdom
.extend(fastdomPromised)
.extend(fastdomSandbox);
var sandbox = myFastdom.sandbox();
var spy1 = sinon.spy();
var spy2 = sinon.spy();
sandbox.measure(spy1)
.then(function() {
return sandbox.mutate(spy2);
})
.then(function() {
sinon.assert.calledOnce(spy1);
sinon.assert.calledOnce(spy2);
spy1.reset();
spy2.reset();
sandbox.measure(spy1);
sandbox.measure(spy2);
sandbox.clear();
raf(function() {
console.log(3);
sinon.assert.notCalled(spy1);
sinon.assert.notCalled(spy2);
done();
});
})
.catch(done);
});
});
================================================
FILE: test/fastdom-strict-test.js
================================================
/*global suite, setup, suiteSetup, suiteTeardown, teardown, test, assert, fastdomPromised*/
/*jshint maxlen:false*/
suite('fastdom-strict', function() {
var raf = window.requestAnimationFrame;
var fastdom;
var el;
suiteSetup(function(done) {
var script = document.createElement('script');
script.src = '/base/fastdom-strict.js';
document.head.appendChild(script);
script.onload = function() {
fastdom = window.fastdom.extend(fastdomPromised);
done();
};
});
suiteTeardown(function() {
fastdom.strict(false);
});
setup(function() {
return fastdom.mutate(function() {
el = document.createElement('div');
el.style.height = '100px';
el.style.width = '100px';
document.body.appendChild(el);
});
});
teardown(function() {
return fastdom.mutate(function() {
el.remove();
});
});
test('measuring throws outside of fastdom', function() {
assert.throws(function() {
return el.clientWidth;
});
});
test('measuring does not throws inside `fastdom.measure()`', function() {
return fastdom.measure(function() {
return el.clientWidth;
});
});
test('mutating throws outside of fastdom', function() {
assert.throws(function() {
el.innerHTML = 'foo';
});
});
test('mutating does not throws inside `fastdom.mutate()`', function() {
return fastdom.mutate(function() {
el.innerHTML = 'foo';
});
});
test('it can be disabled and enabled', function(done) {
fastdom.strict(false);
assert.doesNotThrow(function() {
return el.clientWidth;
});
fastdom.strict(true);
assert.throws(function() {
return el.clientWidth;
});
fastdom.measure(function() {
el.clientWidth;
}).then(done);
});
test('callback is called with correct context when measuring and mutating', function(done) {
var ctx1 = { foo: 'bar' };
var ctx2 = { bar: 'baz' };
var spy1 = sinon.spy();
var spy2 = sinon.spy();
fastdom.measure(spy1, ctx1);
fastdom.mutate(spy2, ctx2);
raf(function() {
assert(spy1.calledOn(ctx1));
assert(spy2.calledOn(ctx2));
done();
});
});
});
================================================
FILE: test/fastdom-test.js
================================================
/*jshint maxlen:false*/
/*global suite, setup, teardown, test, assert, sinon, fastdomSandbox, fastdomPromised*/
suite('fastdom', function() {
var raf = window.requestAnimationFrame;
var fastdom;
setup(function() {
fastdom = new window.fastdom.constructor();
});
test('it runs reads before writes', function(done) {
var read = sinon.spy(function() {
assert(!write.called);
});
var write = sinon.spy(function() {
assert(read.called);
done();
});
fastdom.measure(read);
fastdom.mutate(write);
});
test('it calls all reads together, followed by all writes', function(done) {
var read1 = sinon.spy();
var read2 = sinon.spy();
var write1 = sinon.spy();
var write2 = sinon.spy();
// Assign unsorted
fastdom.measure(read1);
fastdom.mutate(write1);
fastdom.measure(read2);
fastdom.mutate(write2);
// After the queue has been emptied
// check the callbacks were called
// in the correct order.
raf(function() {
assert(read1.calledBefore(read2));
assert(read2.calledBefore(write1));
assert(write1.calledBefore(write2));
done();
});
});
test('it calls a read in the same frame if scheduled inside a read callback', function(done) {
var cb = sinon.spy();
fastdom.measure(function() {
// Schedule a callback for *next* frame
raf(cb);
// Schedule a read callback
// that should be run in the
// current frame checking that
// the RAF callback has not
// yet been fired.
fastdom.measure(function() {
assert(!cb.called);
done();
}, this);
}, this);
});
test('it calls a write in the same frame if scheduled inside a read callback', function(done) {
var cb = sinon.spy();
fastdom.measure(function() {
// Schedule a callback for *next* frame
raf(cb);
// Schedule a read callback
// that should be run in the
// current frame checking that
// the RAF callback has not
// yet been fired.
fastdom.mutate(function() {
assert(!cb.called);
done();
}, this);
}, this);
});
test('it calls a read in the *next* frame if scheduled inside a write callback', function(done) {
var cb = sinon.spy();
fastdom.mutate(function() {
// Schedule a callback for *next* frame
raf(cb);
// Schedule a read that should be
// called in the next frame, meaning
// the test callback should have already
// been called.
fastdom.measure(function() {
assert(cb.called);
done();
}, this);
}, this);
});
test('it does not request a new frame when a write is requested inside a nested read', function(done) {
var callback = sinon.spy();
fastdom.mutate(function() {
fastdom.measure(function() {
// Schedule a callback for *next* frame
raf(callback);
// Schedule a read callback
// that should be run in the
// current frame checking that
// the RAF callback has not
// yet been fired.
fastdom.mutate(function() {
assert(!callback.called);
done();
});
});
});
});
test('it schedules a new frame when a read is requested in a nested write', function(done) {
fastdom.raf = sinon.spy(fastdom, 'raf');
fastdom.measure(function() {
fastdom.mutate(function() {
fastdom.measure(function(){
// Should have scheduled a new frame
assert(fastdom.raf.calledTwice);
done();
});
});
});
});
test('it runs nested reads in the same frame', function(done) {
sinon.spy(fastdom, 'raf');
fastdom.measure(function() {
fastdom.measure(function() {
fastdom.measure(function() {
fastdom.measure(function() {
// Should not have scheduled a new frame
sinon.assert.calledOnce(fastdom.raf);
done();
});
});
});
});
});
test('it runs nested writes in the same frame', function(done) {
fastdom.raf = sinon.spy(fastdom, 'raf');
fastdom.mutate(function() {
fastdom.mutate(function() {
fastdom.mutate(function() {
fastdom.mutate(function() {
// Should not have scheduled a new frame
sinon.assert.calledOnce(fastdom.raf);
done();
});
});
});
});
});
test('it calls a "read" callback with the given context', function(done) {
fastdom.measure(function() {
assert.equal(this.foo, 'bar');
done();
}, { foo: 'bar' });
});
test('it calls a "write" callback with the given context', function(done) {
fastdom.mutate(function() {
assert.equal(this.foo, 'bar');
done();
}, { foo: 'bar' });
});
test('it has an empty job hash when batch complete', function(done) {
var ran = 0;
fastdom.measure(function(){ ran += 1; });
fastdom.measure(function(){ ran += 2; });
fastdom.mutate(function(){ ran += 4; });
fastdom.mutate(function(){ ran += 8; });
// Check there are four jobs stored
assert.equal(ran, 0);
raf(function() {
assert.equal(ran, 15);
done();
});
});
test('it maintains correct context if single method is registered twice', function(done) {
var ctx1 = { foo: 'bar' };
var ctx2 = { bar: 'baz' };
function shared() {}
var spy1 = sinon.spy(shared);
var spy2 = sinon.spy(shared);
fastdom.measure(spy1, ctx1);
fastdom.measure(spy2, ctx2);
raf(function() {
assert(spy1.calledOn(ctx1));
assert(spy2.calledOn(ctx2));
done();
});
});
test('it runs .catch() handler on error if one has been registered', function(done) {
fastdom.catch = sinon.spy();
fastdom.measure(function() { throw 'err1'; });
fastdom.mutate(function() { throw 'err2'; });
raf(function() {
raf(function() {
assert(fastdom.catch.calledTwice, 'twice');
assert(fastdom.catch.getCall(0).calledWith('err1'), 'bla');
assert(fastdom.catch.getCall(1).calledWith('err2'), 'bl2');
done();
});
});
});
suite('exceptions', function() {
// temporarily disable mocha error detection
setup(function() {
this.onerror = window.onerror;
window.onerror = null;
});
// re-enable mocha error detection
teardown(function() {
window.onerror = this.onerror;
});
test('it flushes remaining tasks in next frame if prior task throws', function(done) {
var spy = sinon.spy();
fastdom.measure(function() { throw new Error('error'); });
fastdom.measure(spy);
raf(function() {
sinon.assert.notCalled(spy);
raf(function() {
sinon.assert.calledOnce(spy);
done();
});
});
});
});
test('it stops rAF loop once frame queue is empty', function(done) {
var callback = sinon.spy();
sinon.spy(fastdom, 'raf');
fastdom.measure(callback);
raf(function() {
assert(callback.called);
assert(fastdom.raf.calledOnce);
done();
});
});
suite('clear', function() {
test('it does not run "read" job if cleared (sync)', function(done) {
var read = sinon.spy();
var id = fastdom.measure(read);
fastdom.clear(id);
raf(function() {
raf(function() {
assert(!read.called);
done();
});
});
});
test('it fails silently if job not found in queue', function(done) {
var read = sinon.spy();
var read2 = sinon.spy();
var id = fastdom.measure(read);
fastdom.clear(id);
raf(function() {
assert(!read2.called);
done();
});
});
test('it does not run "write" job if cleared (async)', function(done) {
var read = sinon.spy();
var write = sinon.spy();
var id = fastdom.mutate(write);
fastdom.measure(function() {
fastdom.clear(id);
raf(function() {
assert(!read.called);
done();
});
});
});
test('it does not run "write" job if cleared', function(done) {
var write = sinon.spy();
var id = fastdom.mutate(write);
fastdom.clear(id);
raf(function() {
assert(!write.called);
done();
});
});
test('it removes reference to the job if cleared', function(done) {
var write = sinon.spy();
var id = fastdom.mutate(write);
fastdom.clear(id);
raf(function() {
raf(function() {
raf(function() {
assert(!write.called);
done();
});
});
});
});
});
suite('FastDom#extend()', function() {
test('it has the properties of given object', function() {
var fastdom2 = fastdom.extend({ prop: 'foo' });
assert.equal(fastdom2.prop, 'foo');
});
test('it can extend an extension', function() {
var fastdom2 = fastdom.extend({ prop: 'foo' });
var fastdom3 = fastdom2.extend({ prop: 'bar' });
assert.equal(fastdom2.prop, 'foo');
assert.equal(fastdom3.prop, 'bar');
assert.equal(fastdom2.fastdom, fastdom);
assert.equal(fastdom3.fastdom, fastdom2);
});
test('it throws if argument is not object', function() {
assert.throws(function() {
fastdom.extend();
});
assert.throws(function() {
fastdom.extend('oopsie');
});
assert.throws(function() {
fastdom.extend(999);
});
});
test('it only mixes in own properties', function() {
var proto = { foo: 'foo' };
var extension = Object.create(proto);
var extended = fastdom.extend(extension);
assert.isUndefined(extended.foo);
});
});
});
================================================
FILE: test/karma.conf.js
================================================
'use strict';
module.exports = function(config) {
config.set({
basePath: '..',
browsers: [
'chrome',
'Firefox'
],
frameworks: [
'mocha',
'chai-sinon'
],
reporters: [
'mocha',
'coverage'
],
coverageReporter: {
type : 'lcov',
dir : 'test/',
subdir: 'coverage'
},
preprocessors: {
'fastdom.js': ['coverage'],
'extensions/*.js': ['coverage']
},
client: {
captureConsole: true,
mocha: { ui: 'tdd' }
},
customLaunchers: {
chrome: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},
files: [
'fastdom.js',
'extensions/fastdom-promised.js',
'extensions/fastdom-sandbox.js',
'test/fastdom-sandbox-test.js',
'test/fastdom-promised-test.js',
'test/fastdom-strict-test.js',
'test/fastdom-test.js',
{ pattern: 'fastdom-strict.js', included: false }
]
});
};
================================================
FILE: webpack.config.js
================================================
module.exports = [
{
entry: './src/fastdom-strict.js',
output: {
filename: 'fastdom-strict.js',
library: 'fastdom',
libraryTarget: 'umd'
},
externals: {
'../fastdom': 'fastdom'
}
}
];
gitextract_gaqz58xe/ ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── README.md ├── bower.json ├── examples/ │ ├── animation.html │ └── aspect-ratio.html ├── extensions/ │ ├── fastdom-promised.d.ts │ ├── fastdom-promised.js │ └── fastdom-sandbox.js ├── fastdom-strict.js ├── fastdom.d.ts ├── fastdom.js ├── package.json ├── src/ │ └── fastdom-strict.js ├── test/ │ ├── fastdom-promised-test.js │ ├── fastdom-sandbox-test.js │ ├── fastdom-strict-test.js │ ├── fastdom-test.js │ └── karma.conf.js └── webpack.config.js
SYMBOL INDEX (30 symbols across 6 files)
FILE: extensions/fastdom-promised.js
function create (line 55) | function create(promised, type, fn, ctx) {
FILE: extensions/fastdom-sandbox.js
function Sandbox (line 54) | function Sandbox(fastdom) {
function clearAll (line 118) | function clearAll(fastdom, tasks) {
function remove (line 134) | function remove(array, item) {
FILE: fastdom-strict.js
function __webpack_require__ (line 16) | function __webpack_require__(moduleId) {
function StrictDom (line 328) | function StrictDom(win) {
function Property (line 461) | function Property(object, name, config, strictdom) {
function Measure (line 530) | function Measure() {
function Mutate (line 599) | function Mutate() {
function Accessor (line 662) | function Accessor() {
function Value (line 716) | function Value() {
function Style (line 781) | function Style() {
function ClassList (line 795) | function ClassList() {
function StrictStyle (line 809) | function StrictStyle(el, strictdom) {
function getter (line 857) | function getter(key) {
function setter (line 863) | function setter(key) {
function caller (line 871) | function caller(key) {
function StrictClassList (line 881) | function StrictClassList(el, strictdom) {
function error (line 918) | function error(type) {
function getDescriptor (line 927) | function getDescriptor(object, prop) {
function extend (line 931) | function extend(parent, props) {
function isAttached (line 935) | function isAttached(el) {
FILE: fastdom.d.ts
type Omit (line 1) | type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
class Fastdom (line 3) | class Fastdom {
FILE: fastdom.js
function FastDom (line 39) | function FastDom() {
function scheduleFlush (line 168) | function scheduleFlush(fastdom) {
function flush (line 185) | function flush(fastdom) {
function remove (line 218) | function remove(array, item) {
function mixin (line 230) | function mixin(target, source) {
FILE: test/fastdom-test.js
function shared (line 214) | function shared() {}
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (84K chars).
[
{
"path": ".gitignore",
"chars": 51,
"preview": "node_modules\n.DS_Store\nnpm-debug.log\ntest/coverage\n"
},
{
"path": ".jshintignore",
"chars": 34,
"preview": "node_modules\nfastdom.js\nstrict.js\n"
},
{
"path": ".jshintrc",
"chars": 543,
"preview": "{\n \"camelcase\": false,\n \"forin\": false,\n \"latedef\": \"nofunc\",\n \"newcap\": false,\n \"noarg\": true,\n \"node\": true,\n \""
},
{
"path": ".npmignore",
"chars": 62,
"preview": "**/*.\nbower.json\nwebpack.config.js\nnode_modules\nexamples\ntest\n"
},
{
"path": ".travis.yml",
"chars": 342,
"preview": "sudo: required\ndist: trusty\n\nlanguage: node_js\n\nnode_js:\n - 5.1.0\n\naddons:\n firefox: latest\n apt:\n sources:\n "
},
{
"path": "README.md",
"chars": 7525,
"preview": "# fastdom [](https://twitter.com/wilsonp"
},
{
"path": "bower.json",
"chars": 348,
"preview": "{\n \"name\": \"fastdom\",\n \"description\": \"Eliminates layout thrashing by batching DOM read/write operations\",\n \"main\": \""
},
{
"path": "examples/animation.html",
"chars": 12721,
"preview": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<title>FastDom: Animation Example</title>\n\t\t<script type=\"text/javascript\" src=\"../fast"
},
{
"path": "examples/aspect-ratio.html",
"chars": 3009,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>FastDom: Aspect Ratio Example</title>\n<style>\n\n * {\n box"
},
{
"path": "extensions/fastdom-promised.d.ts",
"chars": 360,
"preview": "declare namespace FastdomPromised {\n export function clear<T extends Promise<any>>(task: T): void;\n export function in"
},
{
"path": "extensions/fastdom-promised.js",
"chars": 1719,
"preview": "!(function() {\n\n/**\n * Wraps fastdom in a Promise API\n * for improved control-flow.\n *\n * @example\n *\n * // returning a "
},
{
"path": "extensions/fastdom-sandbox.js",
"chars": 2823,
"preview": "(function(exports) {\n\n/**\n * Mini logger\n *\n * @return {Function}\n */\nvar debug = 0 ? console.log.bind(console, '[fastdo"
},
{
"path": "fastdom-strict.js",
"chars": 23658,
"preview": "(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object"
},
{
"path": "fastdom.d.ts",
"chars": 431,
"preview": "type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;\n\ndeclare class Fastdom {\n clear<T extends () => void>(t"
},
{
"path": "fastdom.js",
"chars": 5286,
"preview": "!(function(win) {\n\n/**\n * FastDom\n *\n * Eliminates layout thrashing\n * by batching DOM read/write\n * interactions.\n *\n *"
},
{
"path": "package.json",
"chars": 1534,
"preview": "{\n \"name\": \"fastdom\",\n \"description\": \"Eliminates layout thrashing by batching DOM read/write operations\",\n \"version\""
},
{
"path": "src/fastdom-strict.js",
"chars": 1038,
"preview": "'use strict';\n\nvar strictdom = require('strictdom');\nvar fastdom = require('../fastdom');\n\n/**\n * Mini logger\n *\n * @ret"
},
{
"path": "test/fastdom-promised-test.js",
"chars": 1333,
"preview": "/*global suite, setup, test, assert, sinon, fastdomPromised*/\n/*jshint maxlen:false*/\n\nsuite('fastdom-promised', functio"
},
{
"path": "test/fastdom-sandbox-test.js",
"chars": 1933,
"preview": "/*global suite, setup, test, assert, sinon, fastdomSandbox, fastdomPromised*/\n/* jshint maxlen:false */\n\nsuite('fastdom-"
},
{
"path": "test/fastdom-strict-test.js",
"chars": 2206,
"preview": "/*global suite, setup, suiteSetup, suiteTeardown, teardown, test, assert, fastdomPromised*/\n/*jshint maxlen:false*/\n\nsui"
},
{
"path": "test/fastdom-test.js",
"chars": 9844,
"preview": "/*jshint maxlen:false*/\n/*global suite, setup, teardown, test, assert, sinon, fastdomSandbox, fastdomPromised*/\n\nsuite('"
},
{
"path": "test/karma.conf.js",
"chars": 979,
"preview": "'use strict';\n\nmodule.exports = function(config) {\n config.set({\n basePath: '..',\n\n browsers: [\n 'chrome',\n "
},
{
"path": "webpack.config.js",
"chars": 233,
"preview": "module.exports = [\n {\n entry: './src/fastdom-strict.js',\n output: {\n filename: 'fastdom-strict.js',\n li"
}
]
About this extraction
This page contains the full source code of the wilsonpage/fastdom GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (76.2 KB), approximately 24.0k tokens, and a symbol index with 30 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.