diff --git a/@types/abstractions/base-handler.d.ts b/@types/abstractions/base-handler.d.ts index a24adec..3bad777 100644 --- a/@types/abstractions/base-handler.d.ts +++ b/@types/abstractions/base-handler.d.ts @@ -35,18 +35,130 @@ export declare abstract class BaseHandler} [excludeProps] + * @returns {Dictionary} + * + * @memberOf BaseHandler + */ protected GetHTMLProps(excludeProps?: Array): Dictionary; + /** + * Initial open state value. + * By default it gets initial value from props: defaultOpen and open. + * + * @protected + * @returns + * + * @memberOf BaseHandler + */ protected GetInitialOpenValue(): boolean; + /** + * Return true if dropdown is controlled outside of this component. + * + * @protected + * @returns + * + * @memberOf BaseHandler + */ protected IsControlled(): boolean; + /** + * Checks if passed element is in container element. + * + * @protected + * @param {Element} element + * @returns + * + * @memberOf BaseHandler + */ protected IsElementInContainer(element: Element): boolean; + /** + * Handles window click event. + * + * @protected + * + * @memberOf BaseHandler + */ protected OnOutsideClick: (event: MouseEvent) => void; + /** + * Handles window keyboard events. + * + * @private + * + * @memberOf BaseHandler + */ private OnWindowKeyUp; - protected OnHeaderClick: () => void; - protected OnSectionClick: () => void; + /** + * Triggers this method when header is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + protected OnHeaderClick(): void; + /** + * Triggers this method when section is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + protected OnSectionClick(): void; + /** + * Triggers all callbacks: onOpen, onClose and onToggle. + * + * @protected + * @param {boolean} open + * @param {Contracts.EventSource} source + * + * @memberOf BaseHandler + */ protected TriggerCallbacks(open: boolean, source: Contracts.EventSource): void; + /** + * Updates state if dropdown is not controlled. + * + * @protected + * @param {boolean} open + * + * @memberOf BaseHandler + */ protected UpdateOpenState(open: boolean): void; protected SetElementRef: (element: any) => void; + /** + * Checks if top children are BaseHeader and BaseSection based components. + * MUST be used to render children for BaseHandler component. + * + * @protected + * @param {React.ReactNode} children + * @returns + * + * @memberOf BaseHandler + */ + protected RenderChildren(children: React.ReactNode): React.ReactChild[]; } diff --git a/@types/abstractions/base-header.d.ts b/@types/abstractions/base-header.d.ts index a569021..c92c07e 100644 --- a/@types/abstractions/base-header.d.ts +++ b/@types/abstractions/base-header.d.ts @@ -12,5 +12,7 @@ export declare class BaseHeader; }; + constructor(props: TProps, context: BaseHeaderContext); + static SimplrDropdownBaseSection(): void; protected OnHeaderClick(): void; } diff --git a/@types/abstractions/base-section.d.ts b/@types/abstractions/base-section.d.ts index ae59d7b..deabc48 100644 --- a/@types/abstractions/base-section.d.ts +++ b/@types/abstractions/base-section.d.ts @@ -14,6 +14,8 @@ export declare class BaseSection; DropdownOpen: React.Requireable; }; + constructor(props: TProps, context: BaseSectionContext); + static SimplrDropdownBaseHeader(): void; protected OnSectionClick(): void; protected IsOpen(): boolean; } diff --git a/@types/contracts.d.ts b/@types/contracts.d.ts index 8ebb77c..44d16f0 100644 --- a/@types/contracts.d.ts +++ b/@types/contracts.d.ts @@ -4,3 +4,5 @@ export declare enum EventSource { OutsideClick = 24, EscapeClick = 32, } +export declare const BASE_HEADER_FUNC = "SimplrDropdownBaseHeader"; +export declare const BASE_SECTION_FUNC = "SimplrDropdownBaseSection"; diff --git a/@types/utils.d.ts b/@types/utils.d.ts index 7aa7c32..c2762e5 100644 --- a/@types/utils.d.ts +++ b/@types/utils.d.ts @@ -1 +1,3 @@ +/// export declare function UniqueArray(arr: Array): any[]; +export declare function CheckComponentType(component: JSX.Element, type: string): boolean; diff --git a/dist/simplr-dropdown.js b/dist/simplr-dropdown.js index dfcad8d..238ba5e 100644 --- a/dist/simplr-dropdown.js +++ b/dist/simplr-dropdown.js @@ -94,6 +94,8 @@ var EventSource; EventSource[EventSource["OutsideClick"] = 24] = "OutsideClick"; EventSource[EventSource["EscapeClick"] = 32] = "EscapeClick"; })(EventSource = exports.EventSource || (exports.EventSource = {})); +exports.BASE_HEADER_FUNC = "SimplrDropdownBaseHeader"; +exports.BASE_SECTION_FUNC = "SimplrDropdownBaseSection"; /***/ }), @@ -114,10 +116,19 @@ Object.defineProperty(exports, "__esModule", { value: true }); var React = __webpack_require__(0); var Contracts = __webpack_require__(1); var Utils = __webpack_require__(10); +var CHILDREN_ERROR = "simplr-dropdown: (DropdownHandler)" + + " component must have two components as children: DropdownHeader and DropdownSection."; var BaseHandler = (function (_super) { __extends(BaseHandler, _super); function BaseHandler(props) { var _this = _super.call(this, props) || this; + /** + * Handles window click event. + * + * @protected + * + * @memberOf BaseHandler + */ _this.OnOutsideClick = function (event) { var props = _this.props; var open = false; @@ -128,6 +139,13 @@ var BaseHandler = (function (_super) { _this.TriggerCallbacks(open, Contracts.EventSource.OutsideClick); _this.UpdateOpenState(open); }; + /** + * Handles window keyboard events. + * + * @private + * + * @memberOf BaseHandler + */ _this.OnWindowKeyUp = function (event) { var props = _this.props; var open = false; @@ -140,24 +158,6 @@ var BaseHandler = (function (_super) { _this.UpdateOpenState(open); } }; - _this.OnHeaderClick = function () { - var props = _this.props; - var open = !_this.state.Open; - if (!props.toggleOnHeaderClick) { - return; - } - _this.TriggerCallbacks(open, Contracts.EventSource.HeaderClick); - _this.UpdateOpenState(open); - }; - _this.OnSectionClick = function () { - var props = _this.props; - var open = false; - if (!props.closeOnSectionClick) { - return; - } - _this.TriggerCallbacks(open, Contracts.EventSource.SectionClick); - _this.UpdateOpenState(open); - }; _this.SetElementRef = function (element) { _this.Element = element; }; @@ -184,10 +184,17 @@ var BaseHandler = (function (_super) { BaseHandler.prototype.getChildContext = function () { return { DropdownOpen: this.state.Open, - DropdownOnHeaderClickCallback: this.OnHeaderClick, - DropdownOnSectionClickCallback: this.OnSectionClick + DropdownOnHeaderClickCallback: this.OnHeaderClick.bind(this), + DropdownOnSectionClickCallback: this.OnSectionClick.bind(this) }; }; + /** + * To close dropdown. + * + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.Close = function () { if (!this.state.Open) { return; @@ -197,6 +204,13 @@ var BaseHandler = (function (_super) { return state; }); }; + /** + * To close dropdown. + * + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.Open = function () { if (this.state.Open) { return; @@ -206,9 +220,25 @@ var BaseHandler = (function (_super) { return state; }); }; + /** + * Get a boolean if dropdown is open or not. + * + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.IsOpen = function () { return this.state.Open; }; + /** + * This MUST be used if spread props are being used on element. + * + * @protected + * @param {Array} [excludeProps] + * @returns {Dictionary} + * + * @memberOf BaseHandler + */ BaseHandler.prototype.GetHTMLProps = function (excludeProps) { var notHTMLProps = [ "defaultOpen", @@ -232,6 +262,15 @@ var BaseHandler = (function (_super) { } return newProps; }; + /** + * Initial open state value. + * By default it gets initial value from props: defaultOpen and open. + * + * @protected + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.GetInitialOpenValue = function () { var props = this.props; var open = false; @@ -243,13 +282,71 @@ var BaseHandler = (function (_super) { } return open; }; + /** + * Return true if dropdown is controlled outside of this component. + * + * @protected + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.IsControlled = function () { return this.props.open != null; }; + /** + * Checks if passed element is in container element. + * + * @protected + * @param {Element} element + * @returns + * + * @memberOf BaseHandler + */ BaseHandler.prototype.IsElementInContainer = function (element) { var containerElement = this.Element; return containerElement.contains(element); }; + /** + * Triggers this method when header is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + BaseHandler.prototype.OnHeaderClick = function () { + var props = this.props; + var open = !this.state.Open; + if (!props.toggleOnHeaderClick) { + return; + } + this.TriggerCallbacks(open, Contracts.EventSource.HeaderClick); + this.UpdateOpenState(open); + }; + /** + * Triggers this method when section is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + BaseHandler.prototype.OnSectionClick = function () { + var props = this.props; + var open = false; + if (!props.closeOnSectionClick) { + return; + } + this.TriggerCallbacks(open, Contracts.EventSource.SectionClick); + this.UpdateOpenState(open); + }; + /** + * Triggers all callbacks: onOpen, onClose and onToggle. + * + * @protected + * @param {boolean} open + * @param {Contracts.EventSource} source + * + * @memberOf BaseHandler + */ BaseHandler.prototype.TriggerCallbacks = function (open, source) { var props = this.props; if (open && props.onOpen != null) { @@ -262,6 +359,14 @@ var BaseHandler = (function (_super) { props.onToggle(open, source); } }; + /** + * Updates state if dropdown is not controlled. + * + * @protected + * @param {boolean} open + * + * @memberOf BaseHandler + */ BaseHandler.prototype.UpdateOpenState = function (open) { if (this.state.Open !== open && !this.IsControlled()) { @@ -271,6 +376,36 @@ var BaseHandler = (function (_super) { }); } }; + /** + * Checks if top children are BaseHeader and BaseSection based components. + * MUST be used to render children for BaseHandler component. + * + * @protected + * @param {React.ReactNode} children + * @returns + * + * @memberOf BaseHandler + */ + BaseHandler.prototype.RenderChildren = function (children) { + if (React.Children.count(children) !== 2) { + throw new Error(CHILDREN_ERROR); + } + var foundHeader = false; + var foundSection = false; + return React.Children.map(children, function (child) { + if (!foundHeader && + Utils.CheckComponentType(child, Contracts.BASE_HEADER_FUNC)) { + foundHeader = true; + return child; + } + else if (!foundSection && + Utils.CheckComponentType(child, Contracts.BASE_SECTION_FUNC)) { + foundSection = true; + return child; + } + throw new Error(CHILDREN_ERROR); + }); + }; return BaseHandler; }(React.Component)); BaseHandler.defaultProps = { @@ -305,9 +440,15 @@ Object.defineProperty(exports, "__esModule", { value: true }); var React = __webpack_require__(0); var BaseHeader = (function (_super) { __extends(BaseHeader, _super); - function BaseHeader() { - return _super !== null && _super.apply(this, arguments) || this; + function BaseHeader(props, context) { + var _this = _super.call(this, props) || this; + if (context.DropdownOnHeaderClickCallback == null) { + throw new Error("simplr-dropdown: " + _this.constructor.name + + " must be inside DropdownHandler component."); + } + return _this; } + BaseHeader.SimplrDropdownBaseSection = function () { }; BaseHeader.prototype.OnHeaderClick = function () { this.context.DropdownOnHeaderClickCallback(); }; @@ -337,9 +478,16 @@ Object.defineProperty(exports, "__esModule", { value: true }); var React = __webpack_require__(0); var BaseSection = (function (_super) { __extends(BaseSection, _super); - function BaseSection() { - return _super !== null && _super.apply(this, arguments) || this; + function BaseSection(props, context) { + var _this = _super.call(this, props) || this; + if (context.DropdownOnSectionClickCallback == null || + context.DropdownOpen == null) { + throw new Error("simplr-dropdown: (BaseHeader) " + _this.constructor.name + + " must be inside DropdownHandler component."); + } + return _this; } + BaseSection.SimplrDropdownBaseHeader = function () { }; BaseSection.prototype.OnSectionClick = function () { this.context.DropdownOnSectionClickCallback(); }; @@ -386,7 +534,7 @@ var DropdownHandler = (function (_super) { return _super !== null && _super.apply(this, arguments) || this; } DropdownHandler.prototype.render = function () { - return React.createElement("div", __assign({ ref: this.SetElementRef }, this.GetHTMLProps()), this.props.children); + return React.createElement("div", __assign({ ref: this.SetElementRef }, this.GetHTMLProps()), this.RenderChildren(this.props.children)); }; return DropdownHandler; }(base_handler_1.BaseHandler)); @@ -547,6 +695,11 @@ function UniqueArray(arr) { return uniqueArr; } exports.UniqueArray = UniqueArray; +function CheckComponentType(component, type) { + var componentType = component.type; + return (componentType[type] != null); +} +exports.CheckComponentType = CheckComponentType; /***/ }) diff --git a/dist/simplr-dropdown.min.js b/dist/simplr-dropdown.min.js index 1986c38..c0de327 100644 --- a/dist/simplr-dropdown.min.js +++ b/dist/simplr-dropdown.min.js @@ -1 +1 @@ -!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t(require("react"));else if("function"==typeof define&&define.amd)define(["react"],t);else{var n=t("object"==typeof exports?require("react"):e.react);for(var o in n)("object"==typeof exports?exports:e)[o]=n[o]}}(this,function(e){return function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=9)}([function(t,n){t.exports=e},function(e,t){Object.defineProperty(t,"__esModule",{value:!0});!function(e){e[e.HeaderClick=8]="HeaderClick",e[e.SectionClick=16]="SectionClick",e[e.OutsideClick=24]="OutsideClick",e[e.EscapeClick=32]="EscapeClick"}(t.EventSource||(t.EventSource={}))},function(e,t,n){var o=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function o(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}();Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),i=n(1),c=n(10),p=function(e){function t(t){var n=e.call(this,t)||this;return n.OnOutsideClick=function(e){var t=n.props;t.closeOnOutsideClick&&!n.IsElementInContainer(e.toElement)&&(n.TriggerCallbacks(!1,i.EventSource.OutsideClick),n.UpdateOpenState(!1))},n.OnWindowKeyUp=function(e){var t=n.props;t.closeOnEscapeClick&&27===e.keyCode&&t.closeOnEscapeClick&&(n.TriggerCallbacks(!1,i.EventSource.EscapeClick),n.UpdateOpenState(!1))},n.OnHeaderClick=function(){var e=n.props,t=!n.state.Open;e.toggleOnHeaderClick&&(n.TriggerCallbacks(t,i.EventSource.HeaderClick),n.UpdateOpenState(t))},n.OnSectionClick=function(){var e=n.props;e.closeOnSectionClick&&(n.TriggerCallbacks(!1,i.EventSource.SectionClick),n.UpdateOpenState(!1))},n.SetElementRef=function(e){n.Element=e},window.addEventListener("click",n.OnOutsideClick),window.addEventListener("keyup",n.OnWindowKeyUp),n.state={Open:n.GetInitialOpenValue()},n}return o(t,e),t.prototype.componentWillReceiveProps=function(e){null!=e.open&&this.props.open!==e.open&&this.setState(function(t){return t.Open=e.open,t})},t.prototype.componentWillUnmount=function(){window.removeEventListener("click",this.OnOutsideClick),window.removeEventListener("keyup",this.OnWindowKeyUp)},t.prototype.getChildContext=function(){return{DropdownOpen:this.state.Open,DropdownOnHeaderClickCallback:this.OnHeaderClick,DropdownOnSectionClickCallback:this.OnSectionClick}},t.prototype.Close=function(){this.state.Open&&this.setState(function(e){return e.Open=!1,e})},t.prototype.Open=function(){this.state.Open||this.setState(function(e){return e.Open=!0,e})},t.prototype.IsOpen=function(){return this.state.Open},t.prototype.GetHTMLProps=function(e){var t=["defaultOpen","open","onOpen","onClose","onToggle","toggleOnHeaderClick","closeOnOutsideClick","closeOnSectionClick","closeOnEscapeClick"];null!=e&&(t=c.UniqueArray(t.concat(e)));var n={};for(var o in this.props)null!=this.props[o]&&-1===t.indexOf(o)&&(n[o]=this.props[o]);return n},t.prototype.GetInitialOpenValue=function(){var e=this.props,t=!1;return null!=e.defaultOpen&&(t=e.defaultOpen),null!=e.open&&(t=e.open),t},t.prototype.IsControlled=function(){return null!=this.props.open},t.prototype.IsElementInContainer=function(e){return this.Element.contains(e)},t.prototype.TriggerCallbacks=function(e,t){var n=this.props;e&&null!=n.onOpen&&n.onOpen(t),e||null==n.onClose||n.onClose(t),null!=n.onToggle&&n.onToggle(e,t)},t.prototype.UpdateOpenState=function(e){this.state.Open===e||this.IsControlled()||this.setState(function(t){return t.Open=e,t})},t}(r.Component);p.defaultProps={toggleOnHeaderClick:!0,closeOnSectionClick:!1,closeOnOutsideClick:!0,closeOnEscapeClick:!0},p.childContextTypes={DropdownOpen:r.PropTypes.bool,DropdownOnHeaderClickCallback:r.PropTypes.func,DropdownOnSectionClickCallback:r.PropTypes.func},t.BaseHandler=p},function(e,t,n){var o=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function o(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}();Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),i=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return o(t,e),t.prototype.OnHeaderClick=function(){this.context.DropdownOnHeaderClickCallback()},t}(r.Component);i.contextTypes={DropdownOnHeaderClickCallback:r.PropTypes.func},t.BaseHeader=i},function(e,t,n){var o=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function o(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}();Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),i=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return o(t,e),t.prototype.OnSectionClick=function(){this.context.DropdownOnSectionClickCallback()},t.prototype.IsOpen=function(){return this.context.DropdownOpen},t}(r.Component);i.contextTypes={DropdownOnSectionClickCallback:r.PropTypes.func,DropdownOpen:r.PropTypes.bool},t.BaseSection=i},function(e,t,n){var o=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function o(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}(),r=this&&this.__assign||Object.assign||function(e){for(var t,n=1,o=arguments.length;n} [excludeProps] + * @returns {Dictionary} + * + * @memberOf BaseHandler + */ protected GetHTMLProps(excludeProps?: Array): Dictionary { let notHTMLProps: string[] = [ "defaultOpen", @@ -134,6 +167,15 @@ export abstract class BaseHandler { let props: TProps = this.props; let open = false; @@ -171,6 +237,13 @@ export abstract class BaseHandler { let props: TProps = this.props; let open = false; @@ -187,7 +260,14 @@ export abstract class BaseHandler { + /** + * Triggers this method when header is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + protected OnHeaderClick() { let props: TProps = this.props; let open = !this.state.Open; @@ -199,7 +279,14 @@ export abstract class BaseHandler { + /** + * Triggers this method when section is clicked. + * + * @protected + * + * @memberOf BaseHandler + */ + protected OnSectionClick() { let props: TProps = this.props; let open = false; @@ -211,6 +298,15 @@ export abstract class BaseHandler { this.Element = element; } + + /** + * Checks if top children are BaseHeader and BaseSection based components. + * MUST be used to render children for BaseHandler component. + * + * @protected + * @param {React.ReactNode} children + * @returns + * + * @memberOf BaseHandler + */ + protected RenderChildren(children: React.ReactNode) { + if (React.Children.count(children) !== 2) { + throw new Error(CHILDREN_ERROR); + } + + let foundHeader = false; + let foundSection = false; + + return React.Children.map(children, child => { + if (!foundHeader && + Utils.CheckComponentType(child as JSX.Element, Contracts.BASE_HEADER_FUNC)) { + foundHeader = true; + + return child; + } else if (!foundSection && + Utils.CheckComponentType(child as JSX.Element, Contracts.BASE_SECTION_FUNC)) { + foundSection = true; + + return child; + } + + throw new Error(CHILDREN_ERROR); + }); + } } diff --git a/src/abstractions/base-header.ts b/src/abstractions/base-header.ts index 27d4e28..8fb30d5 100644 --- a/src/abstractions/base-header.ts +++ b/src/abstractions/base-header.ts @@ -1,8 +1,8 @@ import * as React from "react"; -export interface BaseHeaderProps {} +export interface BaseHeaderProps { } -export interface BaseHeaderState {} +export interface BaseHeaderState { } export interface BaseHeaderContext { DropdownOnHeaderClickCallback: Function; @@ -16,6 +16,24 @@ export class BaseHeader - {this.props.children} + {this.RenderChildren(this.props.children)} ; } } diff --git a/src/contracts.ts b/src/contracts.ts index 590332b..354dc38 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -4,3 +4,6 @@ export enum EventSource { OutsideClick = 24, EscapeClick = 32 } + +export const BASE_HEADER_FUNC = "SimplrDropdownBaseHeader"; +export const BASE_SECTION_FUNC = "SimplrDropdownBaseSection"; diff --git a/src/utils.ts b/src/utils.ts index 3b99168..e4c6514 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,3 +8,8 @@ export function UniqueArray(arr: Array) { return uniqueArr; } + +export function CheckComponentType(component: JSX.Element, type: string) { + let componentType = component.type as any; + return (componentType[type] != null); +}