diff --git a/README.md b/README.md index 57a0526..bd879c4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,10 @@ Setting up Typester on your page is as easy as: ``` import Typester from 'typester-editor' -new Typester({ el: document.querySelector('[contenteditable]') }) // Where document.querySelector(...) is a single DOM element. +const typesterInstance = new Typester({ el: document.querySelector('[contenteditable]') }) // Where document.querySelector(...) is a single DOM element. + +// If you need to tear down for any reason: +typesterInstance.destroy(); ``` ### Configuration @@ -43,6 +46,15 @@ new Typester({ configs: { toolbar: { buttons: ['bold', 'italic', 'h1', 'h2', 'orderedlist', 'unorderedlist', 'quote', 'link'] + }, + + styles: { + colors: { + flyoutBg: 'rgb(32, 31, 32)', + menuItemIcon: 'rgb(255, 255, 255)', + menuItemHover: 'rgb(0, 174, 239)', + menuItemActive: 'rgb(0, 156, 215)' + } } } }); diff --git a/src/scripts/config/styles.js b/src/scripts/config/styles.js new file mode 100644 index 0000000..7494347 --- /dev/null +++ b/src/scripts/config/styles.js @@ -0,0 +1,8 @@ +export default { + colors: { + flyoutBg: 'rgb(32, 31, 32)', + menuItemIcon: 'rgb(255, 255, 255)', + menuItemHover: 'rgb(0, 174, 239)', + menuItemActive: 'rgb(0, 156, 215)' + } +}; diff --git a/src/scripts/containers/AppContainer.js b/src/scripts/containers/AppContainer.js index a0e65c7..3b42c68 100644 --- a/src/scripts/containers/AppContainer.js +++ b/src/scripts/containers/AppContainer.js @@ -116,6 +116,15 @@ const AppContainer = Container({ */ handleBlur () { // Should the container require to do anything in particular here + }, + + /** + * Destroy the entire Typeset instance + * @func destroy + */ + destroy () { + const { mediator } = this; + mediator.emit('app:destroy'); } } }); diff --git a/src/scripts/containers/UIContainer.js b/src/scripts/containers/UIContainer.js index 39183f9..194ba0b 100644 --- a/src/scripts/containers/UIContainer.js +++ b/src/scripts/containers/UIContainer.js @@ -19,6 +19,7 @@ import Container from '../core/Container'; import Toolbar from '../modules/Toolbar'; import Flyout from '../modules/Flyout'; import Mouse from '../modules/Mouse'; +import Styles from '../modules/Styles'; /** * @constructor UIContainer @@ -32,7 +33,7 @@ const UIContainer = Container({ /** * Child Modules: [{@link modules/Flyout}, {@link modules/Toolbar}] * Note: The Toobar is instantiated with the document body set as it's dom.el. - * @enum {Array<{class:Module}>} modules + * @enum {Array<{class:Module}>} modules */ modules: [ { @@ -48,6 +49,9 @@ const UIContainer = Container({ }, { class: Mouse + }, + { + class: Styles } ] }); diff --git a/src/scripts/core/Container.js b/src/scripts/core/Container.js index e89fc7d..8f03f33 100644 --- a/src/scripts/core/Container.js +++ b/src/scripts/core/Container.js @@ -166,7 +166,9 @@ const Container = function Container(containerObj) { return { setMediatorParent (parentMediator) { mediator.setParent(parentMediator); - } + }, + + destroy: boundMethods.destroy }; } }; diff --git a/src/scripts/core/Mediator.js b/src/scripts/core/Mediator.js index 34a7cd6..216f0b2 100644 --- a/src/scripts/core/Mediator.js +++ b/src/scripts/core/Mediator.js @@ -122,6 +122,10 @@ const Mediator = function (opts={}) { if (requestHandler) { return requestHandler(options); } + }, + + destroy () { + requests.handlers = {}; } }; @@ -152,6 +156,10 @@ const Mediator = function (opts={}) { if (commandHandler) { commandHandler(options); } + }, + + destroy () { + commands.handlers = {}; } }; @@ -176,9 +184,18 @@ const Mediator = function (opts={}) { emit (eventKey, options) { const eventHandlers = events.getHandlers(eventKey); + if (eventHandlers.length) { eventHandlers.forEach((eventHandler) => eventHandler(options)); } + + if (eventKey === 'app:destroy') { + fn.destroy(); + } + }, + + destroy () { + events.handlers = {}; } }; @@ -370,6 +387,12 @@ const Mediator = function (opts={}) { getId () { return internal.id; + }, + + destroy () { + requests.destroy(); + commands.destroy(); + events.destroy(); } }; diff --git a/src/scripts/core/Module.js b/src/scripts/core/Module.js index 33d07e9..2bb8104 100644 --- a/src/scripts/core/Module.js +++ b/src/scripts/core/Module.js @@ -112,6 +112,11 @@ const Module = function (moduleObj) { moduleUtils.bindDomEvents(handlerMethods, context); }, + deregisterDomHandlers (domHandlersMap, context) { + let handlerMethods = moduleUtils.getHandlerMethods(domHandlersMap, context); + moduleUtils.unbindDomEvents(handlerMethods, context); + }, + getHandlerMethods (handlerMap, context) { let routedHandlers = {}; @@ -171,6 +176,22 @@ const Module = function (moduleObj) { }); }, + unbindDomEvents (handlers, context) { + const { dom } = context; + + if (!dom) { + return; + } + + Object.keys(handlers).forEach((eventElKey) => { + const [eventKey, elemKey] = eventElKey.split(' @'); + const elem = elemKey ? dom[elemKey][0] : dom.el[0]; + const eventHandler = handlers[eventElKey]; + + elem.removeEventListener(eventKey, eventHandler); + }); + }, + mergeProps (defaultProps, props={}) { let mergedProps = {}; @@ -248,6 +269,10 @@ const Module = function (moduleObj) { context.setup(); } + context.mediator.registerHandler('event', 'app:destroy', function () { + moduleProto.destroyModule(opts); + }); + if (moduleHandlers) { moduleUtils.registerHandlers(opts.mediator, moduleHandlers, context); } @@ -280,8 +305,12 @@ const Module = function (moduleObj) { } }, - destroyModule () { + destroyModule (opts) { + const { context } = opts; + if (moduleHandlers.domEvents) { + moduleUtils.deregisterDomHandlers(moduleHandlers.domEvents, context); + } } }; diff --git a/src/scripts/modules/Config.js b/src/scripts/modules/Config.js index 3b045c0..b66d82c 100644 --- a/src/scripts/modules/Config.js +++ b/src/scripts/modules/Config.js @@ -1,11 +1,12 @@ import Module from '../core/Module'; import toolbarConfig from '../config/toolbar'; import config from '../config/config'; +import stylesConfig from '../config/styles'; const Config = Module({ name: 'Config', props: {}, - acceptsConfigs: ['toolbar'], + acceptsConfigs: ['toolbar', 'styles'], handlers: { requests: { @@ -16,7 +17,8 @@ const Config = Module({ 'config:toolbar:listTags' : 'getToolbarListTags', 'config:toolbar:preventNewlineDefault' : 'getToolbarPreventNewlineDefault', 'config:blockElementNames' : 'getConfigBlockElementNames', - 'config:defaultBlock' : 'getDefaultBlock' + 'config:defaultBlock' : 'getDefaultBlock', + 'config:styles': 'getStyles' } }, @@ -70,6 +72,13 @@ const Config = Module({ getDefaultBlock () { return config.defaultBlock; + }, + + getStyles () { + const { configs } = this; + return { + colors: Object.assign({}, stylesConfig.colors, configs.styles.colors) + }; } } }); diff --git a/src/scripts/modules/Styles.js b/src/scripts/modules/Styles.js new file mode 100644 index 0000000..fdc9017 --- /dev/null +++ b/src/scripts/modules/Styles.js @@ -0,0 +1,104 @@ +// jshint strict: false + +/** + * Styles - + * Handle the creation and embedding of custom styles. + * @access protected + * @module modules/Styles + */ + +import Module from '../core/Module'; + +const Styles = Module({ + name: 'Styles', + + props: { + stylesheet: null, + }, + + handlers: { + events: { + 'app:destroy': 'destroy' + } + }, + + methods: { + setup () { + this.createStylesheet(); + }, + + init () { + const { mediator } = this; + const config = mediator.get('config:styles'); + let stylesheetContent = this.stylesTemplate(config); + + this.appendStylesheet(); + this.updateStylesheet(stylesheetContent); + }, + + stylesTemplate (config) { + return ` + .typester-toolbar .typester-menu-item, + .typester-input-form input[type=text], + .typester-link-display a, + .typester-input-form button { + color: ${config.colors.menuItemIcon}; + } + + .typester-toolbar .typester-menu-item svg, + .typester-link-display .typester-link-edit svg, + .typester-input-form button svg { + fill: ${config.colors.menuItemIcon}; + } + + .typester-input-form button svg { + stroke: ${config.colors.menuItemIcon}; + } + + .typester-toolbar .typester-menu-item:hover, + .typester-link-display .typester-link-edit:hover + .typester-input-form button:hover { + background: ${config.colors.menuItemHover}; + } + + .typester-toolbar .typester-menu-item.s--active { + background: ${config.colors.menuItemActive}; + } + + .typester-flyout .typester-flyout-content { + background: ${config.colors.flyoutBg}; + } + + .typester-flyout.place-above .typester-flyout-arrow { + border-top-color: ${config.colors.flyoutBg}; + } + + .typester-flyout.place-below .typester-flyout-arrow { + border-bottom-color: ${config.colors.flyoutBg}; + } + `; + }, + + createStylesheet () { + this.stylesheet = document.createElement('style'); + }, + + appendStylesheet () { + document.head.appendChild(this.stylesheet); + }, + + updateStylesheet (stylesheetContent) { + this.stylesheet.textContent = stylesheetContent; + }, + + removeStylesheet () { + document.head.removeChild(this.stylesheet); + }, + + destroy () { + this.removeStylesheet(); + } + } +}); + +export default Styles; diff --git a/src/templates/inputForm.html b/src/templates/inputForm.html index 5c6abd0..8d7b77c 100644 --- a/src/templates/inputForm.html +++ b/src/templates/inputForm.html @@ -3,7 +3,7 @@ +
Inspect Content Inspect Canvas @@ -114,12 +117,20 @@

Typester test server

@@ -130,6 +141,7 @@

Typester test server

const contentInspector = document.getElementById('content-inspector'); const inspectorTools = document.querySelectorAll('.inspector-tools a'); + const destroyBtn = document.querySelector('.destroy-btn'); inspectorTools.forEach(function (inspectorTool, index) { if (index === 0) { @@ -222,7 +234,10 @@

Typester test server

editorObserver.observe(contentEditable, observerConfig); canvasObserver.observe(document.querySelector('.typester-canvas'), observerConfig); updateInspector(); - // requestAnimationFrame(updateInspector); + + destroyBtn.addEventListener('click', function () { + typesterInstance.destroy(); + }); })();