UNPKG

decks

Version:

JavaScript UI library for viewing collections of items.

248 lines (211 loc) 7.1 kB
var _ = require("lodash"); var binder = require("./events").binder; var hasEmitter = require("./events").hasEmitter; var DecksEvent = require("./events").DecksEvent; var rect = require("./utils").rect; var dom = require("./ui").dom; var validate = require("./utils/validate"); /** * Represents the Viewport Frame. The Frame is essentially a DOM element which acts as the * visible portion of the decks system. The Frame is always set to position relative, and * overflow hidden. The Frame contains a Canvas (element), which can move around within the * Frame element. The Frame crops the content of the Canvas at the Frame edges. * * @class * @mixes binder * @mixes hasEmitter * @param {!Object} options Frame options * @param {!HTMLElement} options.element Frame container element * @param {?(Canvas|Object)} options.canvas Frame canvas instance or options */ function Frame(options) { if (!(this instanceof Frame)) { return new Frame(options); } options = _.merge({}, this.defaultOptions, options); this.frameId = _.uniqueId(); this.position = options.position; this.overflow = options.overflow; this.watchWindowResize = options.watchWindowResize; this.watchWindowScroll = options.watchWindowScroll; this.debouncedOnWindowResize = _.debounce(this.onWindowResize, options.debouncedWindowResizeWait); this.debouncedOnWindowScroll = _.debounce(this.onWindowScroll, options.debouncedWindowScrollWait); this.setConfig(options.config); this.setEmitter(options.emitter); this.setElement(options.element); this.bind(); this.emit(DecksEvent("frame:ready", this)); } _.extend(Frame.prototype, binder, hasEmitter, /** @lends Frame.prototype */ { defaultOptions: { position: "relative", overflow: "auto", watchWindowResize: true, watchWindowScroll: true, debouncedWindowResizeWait: 200, debouncedWindowScrollWait: 200 }, /** * Gets the {@link Emitter} events map. * * @return {Object} */ getEmitterEvents: function() { return { "canvas:element:set": "onCanvasElementSet", "deck:resize": "onDeckResize" }; }, /** * Gets the window events map. * * @return {Object} */ getWindowEvents: function() { var map = {}; if (this.watchWindowResize) { map.resize = "debouncedOnWindowResize"; } if (this.watchWindowScroll) { map.scroll = "debouncedOnWindowScroll"; } return map; }, /** * Binds the {@link Emitter} and window events. * * @return {undefined} */ bind: function bind() { this.bindEvents(this.emitter, this.getEmitterEvents()); this.bindEvents(window, this.getWindowEvents()); }, /** * Unbinds the {@link Emitter} and window events. * * @return {undefined} */ unbind: function unbind() { this.unbindEvents(this.emitter, this.getEmitterEvents()); this.unbindEvents(window, this.getWindowEvents()); }, /** * Destroys the {@link Frame} * * @return {undefined} */ destroy: function() { this.unbind(); }, /** * Sets the config object. * * @param config * @return {undefined} */ setConfig: function setConfig(config) { validate(config, "config", { isPlainObject: true, isNotSet: this.config }); this.config = config; }, /** * Sets the Frame's DOM element (container) * * @param {HTMLElement} element the Frame's main container element * @param {Object} options frame options * @return {undefined} */ setElement: function setElement(element) { validate(element, "element", { isElement: true, isNotSet: this.element }); this.element = element; if (!element.id) { this.element.id = this.config.frameClassName + "-" + this.frameId; } dom.addClass(this.element, this.config.frameClassName); // Frame must be positioned (absolute, relative or fixed), so that the Canvas can be positioned within it if (!dom.isPositioned(this.element)) { dom.setStyle(this.element, "position", this.position); } dom.setStyle(this.element, "overflow", this.overflow); this.emit(DecksEvent("frame:element:set", this, element)); this.setBounds(); }, /** * Sets the frame size parameters * * @param options * @return {undefined} */ setBounds: function setBounds() { var bounds = rect.normalize(this.element); // Ignore empty or unchanging bounds if (rect.isEmpty(bounds) || rect.isEqual(this.bounds, bounds)) { return; } this.emit(DecksEvent("frame:bounds:setting", this, { oldBounds: this.bounds, newBounds: bounds })); this.bounds = bounds; this.emit(DecksEvent("frame:bounds:set", this, this.bounds)); }, /** * Indicates if the given element is currently visible within the * Frame's container element. This might be a combination of the element's * bounding rect being inside the Frame element, and stacking like z-index. * * @param {HTMLElement} element element to check for visibility * @return {boolean} whether the element is visible in the Frame */ isElementVisible: function isElementVisible(element) { validate(element, "element", { isElement: true }); return rect.intersects(this.element, element); }, /** * Called when the {@link Canvas} element has been set. * * This appends the {@link Canvas} element in the {@link Frame} element, and also * initializes the {@link Canvas} with the {@link Frame} and {@link Frame} bounds. * * @param e * @return {undefined} */ onCanvasElementSet: function onCanvasElementSet(e) { var canvas = e.sender; var canvasElement = e.data; // Add the Canvas element to the Frame element dom.empty(this.element); dom.append(this.element, canvasElement); // Set the initial canvas bounds to match the frame // TODO: there might be a better way to do this - the problem is that the Frame bounds are // set and an event is emitted before the Canvas has been instantiated, and there is somewhat // of a circular dependency between Frame and Canvas. canvas.setFrame(this); canvas.setFrameBounds(this.bounds); }, /** * Called when a deck:resize event is received. This event is used by the caller * to request that the {@link Frame} re-calculate it's bounds. If the {@link Frame} * bounds have changed, it will usually trigger a render cycle in the {@link Viewport}. * * @return {undefined} */ onDeckResize: function onDeckResize() { this.setBounds(); }, /** * Called on window resize event. Causes the {@link Frame} to re-calculate its bounds, * which might result in a render cycle in the {@link Viewport}. * * @return {undefined} */ onWindowResize: function onWindowResize() { this.setBounds(); }, /** * Called on window scroll event. Causes the {@link Frame} to re-calculate its bounds, * which might result in a render cycle in the {@link Viewport}. * * @return {undefined} */ onWindowScroll: function onWindowScroll() { this.setBounds(); } }); module.exports = Frame;