diff options
Diffstat (limited to 'fastdom-strict.js')
-rw-r--r-- | fastdom-strict.js | 960 |
1 files changed, 960 insertions, 0 deletions
diff --git a/fastdom-strict.js b/fastdom-strict.js new file mode 100644 index 0000000..007521d --- /dev/null +++ b/fastdom-strict.js @@ -0,0 +1,960 @@ +(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(task, ctx) { + debug('measure'); + return this.fastdom.measure(function() { + if (!enabled) return task(); + return strictdom.phase('measure', task); + }, ctx); + }, + + mutate: function(task, ctx) { + debug('mutate'); + 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__; + +/***/ } +/******/ ]) +}); +;
\ No newline at end of file |