/** * @typedef {Object} HeaderData * @description Tool's input and output data format * @property {String} text — Header's content * @property {number} level - Header's level from 1 to 3 */ /** * @typedef {Object} HeaderConfig * @description Tool's config from Editor * @property {string} placeholder — Block's placeholder */ /** * Header block for the CodeX Editor. * * @author CodeX Team (team@ifmo.su) * @copyright CodeX Team 2018 * @license The MIT License (MIT) * @version 2.0.0 */ class Header { /** * Should this tools be displayed at the Editor's Toolbox * @returns {boolean} * @public */ static get displayInToolbox() { return true; } /** * Class for the Toolbox icon * @returns {string} * @public */ static get iconClassName() { return 'cdx-header-icon'; } /** * Render plugin`s main Element and fill it with saved data * @param {HeaderData} blockData - previously saved data * @param {HeaderConfig} blockConfig - Tool's config from Editor */ constructor(blockData = {}, blockConfig = {}) { /** * Styles * @type {Object} */ this._CSS = { wrapper: 'ce-header', settingsButton: 'ce-settings__button', settingsSelected: 'ce-settings__button--selected', }; /** * Tool's settings passed from Editor * @type {HeaderConfig} * @private */ this._settings = blockConfig; /** * Block's data * @type {HeaderData} * @private */ this._data = blockData || {}; /** * List of settings buttons * @type {HTMLElement[]} */ this.settingsButtons = []; /** * Main Block wrapper * @type {HTMLElement} * @private */ this._element = this.getTag(); } /** * Return Tool's view * @returns {HTMLHeadingElement} * @public */ render() { return this._element; } /** * Create Block's settings block * * @return {HTMLElement} */ renderSettings() { let holder = document.createElement('DIV'); /** Add type selectors */ this.levels.forEach( level => { let selectTypeButton = document.createElement('SPAN'); selectTypeButton.classList.add(this._CSS.settingsButton); /** * Highlight current level button */ if (this.currentLevel.number === level.number) { selectTypeButton.classList.add(this._CSS.settingsSelected); } /** * Add SVG icon */ selectTypeButton.innerHTML = level.svg; /** * Save level to its button */ selectTypeButton.dataset.level = level.number; /** * Set up click handler */ selectTypeButton.addEventListener('click', () => { this.setLevel(level.number); }); /** * Append settings button to holder */ holder.appendChild(selectTypeButton); /** * Save settings buttons */ this.settingsButtons.push(selectTypeButton); }); return holder; } /** * Callback for Block's settings buttons * @param level */ setLevel(level) { this.data = { level: level }; /** * Highlight button by selected level */ this.settingsButtons.forEach(button => { button.classList.toggle(this._CSS.settingsSelected, parseInt(button.dataset.level) === level); }); } /** * Method that specified how to merge two Text blocks. * Called by CodeX Editor by backspace at the beginning of the Block * @param {HeaderData} data * @public */ merge(data) { let newData = { text: this.data.text + data.text, level: this.data.level }; this.data = newData; } /** * Validate Text block data: * - check for emptiness * * @param {HeaderData} blockData — data received after saving * @returns {boolean} false if saved data is not correct, otherwise true * @public */ validate(blockData) { return blockData.text.trim() !== ''; } /** * Extract Tool's data from the view * @param {HTMLHeadingElement} toolsContent - Text tools rendered view * @returns {HeaderData} - saved data * @public */ save(toolsContent) { /** * @todo sanitize data */ return { text: toolsContent.innerHTML, level: this.currentLevel.number }; } /** * Get current Tools`s data * @returns {HeaderData} Current data * @private */ get data() { this._data.text = this._element.innerHTML; this._data.level = this.currentLevel.number; return this._data; } /** * Store data in plugin: * - at the this._data property * - at the HTML * * @param {HeaderData} data — data to set * @private */ set data(data) { this._data = data || {}; /** * If level is set and block in DOM * then replace it to a new block */ if (data.level !== undefined && this._element.parentNode) { /** * Create a new tag * @type {HTMLHeadingElement} */ let newHeader = this.getTag(); /** * Save Block's content */ newHeader.innerHTML = this._element.innerHTML; /** * Replace blocks */ this._element.parentNode.replaceChild(newHeader, this._element); /** * Save new block to private variable * @type {HTMLHeadingElement} * @private */ this._element = newHeader; } /** * If data.text was passed then update block's content */ if (data.text !== undefined) { this._element.innerHTML = this._data.text || ''; } } /** * Get tag for target level * By default returns second-leveled header * @return {HTMLElement} */ getTag() { /** * Create element for current Block's level */ let tag = document.createElement(this.currentLevel.tag); /** * Add text to block */ tag.innerHTML = this._data.text || ''; /** * Add styles class */ tag.classList.add(this._CSS.wrapper); /** * Make tag editable */ tag.contentEditable = 'true'; /** * Add Placeholder */ tag.dataset.placeholder = this._settings.placeholder || ''; return tag; } /** * Get current level * @return {level} */ get currentLevel() { let level = this.levels.find( level => level.number === this._data.level); if (!level) { level = this.levels[0]; } return level; } /** * @typedef {object} level * @property {number} number - level number * @property {string} tag - tag correspondes with level number * @property {string} svg - icon */ /** * Available header levels * @return {level[]} */ get levels() { return [ { number: 2, tag: 'H2', svg: '' }, { number: 3, tag: 'H3', svg: '' }, { number: 4, tag: 'H4', svg: '' } ]; } }