UNPKG

custom-code-editor-a3

Version:
553 lines (494 loc) 19.7 kB
import _ from 'lodash'; import ClipboardJS from 'clipboard'; /** * @model CustomCodeEditorMixin * @desc Mixin for Custom Code Editor */ export default { methods: { /** * @method init * @desc Init Custom Code Editor * @param {HTMLElement} element * @return {aceEditor} Return editor intialized from ACE JS */ init(element) { const self = this; // Set Default Submit Value this.setDefaultSubmitValue(); // Default Empty Value this.beforeInit(element); const editor = this.editor(element); this.setEditor(editor); editor.setValue(''); editor.session.setMode(this.ace.aceModePath + this.ace.defaultMode.toLowerCase()); editor.setTheme(this.ace.aceThemePath + this.ace.theme); if (apos.customCodeEditor.browser.mode === 'production') { editor.setOption('useWorker', false); } editor.setOption('enableEmmet', false); // Register Editor Events this.editorEvents.call(self, editor); // Merge Config this.mergeConfig(); // Editor Configurations if (_.has(this.ace.config, 'editorHeight') || _.has(this.ace.config, 'fontSize')) { this.configEditor(); } // Options reference: https://github.com/ajaxorg/ace/wiki/Configuring-Ace if (_.has(this.ace, 'options')) { const options = this.ace.options; for (const key in options) { // eslint-disable-next-line no-prototype-builtins if (options.hasOwnProperty(key)) { editor.setOptions(apos.util.assign(options)); } } } // Enable dropdown if (_.has(this.ace, 'config.dropdown') && _.has(this.ace, 'config.dropdown.enable') && this.ace.config.dropdown.enable) { this.setDropdown(); this.configDropdown(); } if (ClipboardJS.isSupported() && (!_.has(this.ace, 'config.optionsCustomizer.enable') || !_.has(this.field, 'ace.config.optionsCustomizer.enable'))) { // Init ClipboardJS. Need to make sure that optionsCustomizer is enable so that we can initialize ClipboardJS if (this.$el.querySelector('button.copy-options')) { this.initCopyClipboard(this.$el.querySelector('button.copy-options')); } } else if (!ClipboardJS.isSupported()) { console.warn('ClipboardJS is not supported in this browser'); } // Invoke after init this.afterInit(element); return editor; }, /** * @method initCopyClipboard * @desc Init ClipboardJS * @param {HTMLElement} copyButton */ initCopyClipboard(copyButton) { this.clipboard = new ClipboardJS(copyButton); this.clipboardEvents(this.clipboard); }, /** * @method clipboardEvents * @desc Register Clipboard events * @param {ClipboardJS} clipboard */ clipboardEvents(clipboard) { clipboard.on('success', this.successClipboard); clipboard.on('error', this.errorClipboard); }, /** * @method successClipboard * @desc Success Clipboard Event * @param {ClipboardJS.Event} e */ successClipboard(e) { apos.notify('"' + this.field.name + '" field: ' + 'Options Copied!', { type: 'success', dismiss: true }); e.clearSelection(); }, /** * @method errorClipboard * @desc Error Clipboard Event * @param {ClipboardJS.Event} e */ errorClipboard(e) { console.error('Action: ', e.action); console.error('Trigger: ', e.trigger); apos.notify('Unable to copy options', { type: 'error', dismiss: true }); }, /** * @method destroyClipboard * @desc Destroy Clipboard event */ destroyClipboard() { if (this.clipboard) { this.clipboard.off('success', this.successClipboard); this.clipboard.off('error', this.errorClipboard); this.clipboard.destroy(); } }, /** * @method setDropdown * @desc To Set Dropdown if dropdown enable */ setDropdown() { const editor = this.getEditor(); const self = this; // Save new value when press save command editor.commands.addCommand({ name: 'saveNewCode', bindKey: { win: (_.has(this.ace, 'config.saveCommand.win')) ? this.ace.config.saveCommand.win : 'Ctrl-Shift-S', mac: (_.has(this.ace, 'config.saveCommand.mac')) ? this.ace.config.saveCommand.mac : 'Command-Shift-S' }, exec: function (editor) { // If Two or more editor in single schema , show field name if (self.$root.$el.querySelectorAll('.editor-container').length > 1) { apos.notify((_.has(self.ace, 'config.saveCommand.message')) ? self.ace.config.saveCommand.message + ' - Field Name : ' + self.field.name : 'Selected Code Saved Successfully' + ' - Field Name : ' + self.field.name, { type: 'success', dismiss: 2 }); } else { apos.notify((_.has(self.ace, 'config.saveCommand.message')) ? self.ace.config.saveCommand.message : 'Selected Code Saved Successfully', { type: 'success', dismiss: 2 }); } self.originalValue = editor.getSelectedText(); }, readOnly: false }); // Remove Save Command if null if (_.has(this.field, 'ace.config.saveCommand') && this.field.ace.config.saveCommand === null) { editor.commands.removeCommand('saveNewCode'); } // create dropdown modes for (let i = 0; i < this.ace.modes.length; i++) { // Set defaultMode if found defined modes if (self.ace.defaultMode.toLowerCase() === self.ace.modes[i].name.toLowerCase()) { editor.session.setMode('ace/mode/' + self.ace.defaultMode.toLowerCase()); this.$el.querySelector('.dropdown-title').innerText = this.next.type.length > 0 ? this.next.type : self.ace.defaultMode; if (self.ace.modes[i].snippet && !self.ace.modes[i].disableSnippet) { let beautify = ace.require('ace/ext/beautify'); editor.session.setValue(self.ace.modes[i].snippet); beautify.beautify(editor.session); // Find the template for replace the code area const find = editor.find('@code-here', { backwards: false, wrap: true, caseSensitive: true, wholeWord: true, regExp: false }); // If found if (find) { editor.replace(''); } } } } }, /** * @method configDropdown * @desc CSS Config for Dropdown. Only accept this config object: * ```js * ace: { * config: { * dropdown: { * enable: <Boolean>, * height: <Number or String>, * borderRadius: <Number or String>, * fontFamily: <String>, * fontSize: <Number or String>, * backgroundColor: <String>, * textColor: <String>, * position: { * top: <Number or String>, * bottom: <Number or String>, * right: <Number or String>, * left: <Number or String> * }, * arrowColor: <String (HEX or RGB or RGBA)> * } * } * ``` */ configDropdown() { // Assign Styles to new object const styles = _.assign({}, _.omitBy({ height: (_.has(this.ace.config.dropdown, 'height')) ? _.isNumber(this.ace.config.dropdown.height) ? this.ace.config.dropdown.height + 'px' : this.ace.config.dropdown.height : null, borderRadius: (_.has(this.ace.config.dropdown, 'borderRadius')) ? _.isNumber(this.ace.config.dropdown.borderRadius) ? this.ace.config.dropdown.borderRadius + 'px' : this.ace.config.dropdown.borderRadius : null, boxShadow: (_.has(this.ace.config.dropdown, 'boxShadow')) ? this.ace.config.dropdown.boxShadow : null, width: (_.has(this.ace.config.dropdown, 'width')) ? _.isNumber(this.ace.config.dropdown.width) ? this.ace.config.dropdown.width + 'px' : this.ace.config.dropdown.width : null, backgroundColor: (_.has(this.ace.config.dropdown, 'backgroundColor')) ? this.ace.config.dropdown.backgroundColor : null }, _.isNil), _.has(this.ace.config.dropdown, 'position') && { position: _.omitBy({ top: (_.has(this.ace.config.dropdown, 'position.top')) ? _.isNumber(this.ace.config.dropdown.position.top) ? this.ace.config.dropdown.position.top + 'px' : this.ace.config.dropdown.position.top : null, left: (_.has(this.ace.config.dropdown, 'position.left')) ? _.isNumber(this.ace.config.dropdown.position.left) ? this.ace.config.dropdown.position.left + 'px' : this.ace.config.dropdown.position.left : null, right: (_.has(this.ace.config.dropdown, 'position.right')) ? _.isNumber(this.ace.config.dropdown.position.right) ? this.ace.config.dropdown.position.right + 'px' : this.ace.config.dropdown.position.right : null, bottom: (_.has(this.ace.config.dropdown, 'position.bottom')) ? _.isNumber(this.ace.config.dropdown.position.bottom) ? this.ace.config.dropdown.position.bottom + 'px' : this.ace.config.dropdown.position.bottom : null }, _.isNil) }); const titleStyles = _.assign({}, _.omitBy({ color: (_.has(this.ace.config.dropdown, 'textColor')) ? this.ace.config.dropdown.textColor : null, fontFamily: (_.has(this.ace.config.dropdown, 'fontFamily')) ? this.ace.config.dropdown.fontFamily : null, fontSize: (_.has(this.ace.config.dropdown, 'fontSize')) ? this.ace.config.dropdown.fontSize : null }, _.isNil)); // Loop & assign dropdown style for (let prop of Object.keys(styles)) { if (Object.prototype.hasOwnProperty.call(styles, prop)) { if (typeof styles[prop.toString()] === 'object' && Object.keys(styles[prop.toString()]).length > 0) { for (let innerProp of Object.keys(styles[prop.toString()])) { this.$el.querySelector('.dropdown').style[innerProp.toString()] = styles[prop.toString()][innerProp.toString()]; } } this.$el.querySelector('.dropdown').style[prop.toString()] = styles[prop.toString()]; } } // Loop & assign dropdown-title style for (let prop of Object.keys(titleStyles)) { if (Object.prototype.hasOwnProperty.call(titleStyles, prop)) { this.$el.querySelector('.dropdown-title').style[prop.toString()] = titleStyles[prop.toString()]; } } }, /** * @method mergeConfig * @desc to merge any `this.field` specific schema options to merge with project level module options */ mergeConfig() { let merge = false; let mergeOptions = {}; // Customize field options if any. Only allow some override options to be available if (_.has(this.field, 'ace')) { mergeOptions = { defaultMode: _.has(this.field, 'ace.defaultMode') ? this.field.ace.defaultMode.toLowerCase() : this.ace.defaultMode.toLowerCase(), // Let the devs to set undefined on config property if any, else just override it config: !_.has(this.field, 'ace.config') ? null : _.assign({}, this.ace.config, this.field.ace.config) }; let cloneAce = _.cloneDeep(this.ace); // Non-Immutable Copies this.ace = _.mergeWith( {}, cloneAce, mergeOptions, (a, b) => b === null ? null : b ); merge = true; } if (merge) { // Clone to new object const cloneOptions = _.cloneDeep(apos.customCodeEditor.browser.ace); // Remove unnecessary arrays/objects that will affect web performance for (let optionsName of Object.keys(cloneOptions)) { if (!Object.prototype.hasOwnProperty.call(mergeOptions, optionsName)) { delete cloneOptions[optionsName]; } } // Add with specific schema options apos.customCodeEditor.browser = { ...apos.customCodeEditor.browser, // Store all merged that has override on schema level options into `fieldAce`. fieldAce: { [this.field.name]: _.merge({}, cloneOptions, mergeOptions) } }; } }, /** * @method configEditor * @desc CSS Config for AceJS Editor. Only accept this config object: * ```js * ace: { * config: { * editorHeight: <Number or String>, * fontSize: <Number or String> * } * } * ``` * */ configEditor() { const editorStyles = _.assign({}, _.omitBy({ height: (_.has(this.ace.config, 'editorHeight')) ? _.isNumber(this.ace.config.editorHeight) ? this.ace.config.editorHeight + 'px' : this.ace.config.editorHeight : null, fontSize: (_.has(this.ace.config, 'fontSize')) ? _.isNumber(this.ace.config.fontSize) ? this.ace.config.fontSize + 'px' : this.ace.config.fontSize : null }, _.isNil)); // Loop & assign editor style for (let prop of Object.keys(editorStyles)) { if (Object.prototype.hasOwnProperty.call(editorStyles, prop)) { this.$el.querySelector('[data-editor]').style[prop.toString()] = editorStyles[prop.toString()]; } } }, /** * @method filterModesList * @desc Filter Modes by Search Input Event * @param {Event} e */ filterModesList(e) { let input, filter, li, i, div, txtValue; // eslint-disable-next-line prefer-const input = e.currentTarget; // eslint-disable-next-line prefer-const filter = input.value.toUpperCase(); // eslint-disable-next-line prefer-const div = this.$el.querySelector('.dropdown-content'); // eslint-disable-next-line prefer-const li = div.querySelectorAll('li'); for (i = 0; i < li.length; i++) { (function (i) { txtValue = li[i].innerText; if (txtValue.toUpperCase().indexOf(filter) > -1) { li[i].style.display = ''; } else { li[i].style.display = 'none'; } }(i)); } }, /** * @method changeMode * @desc Change Mode event when mode dropdown is clicked * @param {Event} e Event Listener * @return {null} Return nothing because it will automatically assign Mode to editor */ changeMode(e) { const getText = e.currentTarget.getAttribute('data-name'); const getTitle = e.currentTarget.getAttribute('data-title'); const editor = this.getEditor(); this.$el.querySelector('.dropdown-title').innerText = ((getTitle) || this.getName(getText)); for (let i = 0; i < this.ace.modes.length; i++) { if (getText === this.ace.modes[i].name.toLowerCase()) { editor.session.setMode('ace/mode/' + this.ace.modes[i].name.toLowerCase()); if (this.ace.modes[i].snippet) { // If got disableContent , get out from this if else if (this.ace.modes[i].disableSnippet) { return; } let beautify = ace.require('ace/ext/beautify'); editor.session.setValue(this.ace.modes[i].snippet); beautify.beautify(editor.session); // If changing mode got existing codes , replace the value if (editor.getSelectedText().length > 1) { this.originalValue = editor.replace(editor.getSelectedText()); return; } // Find the template for replace the code area const find = editor.find('@code-here', { backwards: false, wrap: true, caseSensitive: true, wholeWord: true, regExp: false }); // If found if (find && !_.isUndefined(this.originalValue)) { editor.replace(this.originalValue); } else { editor.replace(''); } } } } }, /** * @method setEditor * @desc Set Editor * @param {aceEditor} editor */ setEditor(editor) { apos.customCodeEditor.browser.editor = apos.util.assign({}, apos.customCodeEditor.browser.editor, { [this.field.name]: editor }); }, /** * @method getEditor * @desc Get Editor * @return {aceEditor} Get Editor or Null */ getEditor() { if (_.has(apos.customCodeEditor.browser, `editor.${this.field.name}`)) { return apos.customCodeEditor.browser.editor[this.field.name]; } return null; }, /** * @method editor * @desc Initialize editor * @param {HTMLElement} element * @return {aceEditor} Return initialized AceJS Editor from Element */ editor(element) { return ace.edit(element); }, /** * @method getName * @desc To convert any 'camelCase' name to 'Camel Case' * @param {String} name * @return {String} String that has been trimmed and modified */ getName(name) { return name.replace(/(_|-)/g, ' ') .trim() .replace(/\w\S*/g, function (str) { return str.charAt(0).toUpperCase() + str.substr(1); }) .replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') .trim(); }, /** * @method setDefaultSubmitValue * @desc To set default Submit value for `this.next` that will contains: * ```js * this.next = { * code: '', * type: '' * } * ``` */ setDefaultSubmitValue() { if (!_.isObject(this.next)) { this.next = { code: '', type: '' }; } else { // Assign to original value; this.originalValue = this.next.code; } }, /** * @method setEditorValue * @desc Set editor value on `mounted` function. */ setEditorValue() { const editor = this.getEditor(); if (_.isObject(this.next) && (_.has(this.next, 'code') && !_.isEmpty(this.next.code)) && (_.has(this.next, 'type') && !_.isEmpty(this.next.type))) { editor.session.setValue(this.next.code); editor.session.setMode('ace/mode/' + this.next.type.toLowerCase()); } }, /** * @method setSubmitValue * @desc To set submit value whenever editor is blur * @param {aceEditor} editor */ setSubmitValue(editor) { const mode = editor.session.getMode().$id.match(/(?!(\/|\\))(?:\w)*$/g)[0]; if (editor.getValue() !== this.next.code) { this.next.code = editor.getValue(); } if ((editor.getValue().length > 0 || this.next.type.length > 0) && mode !== this.next.type) { this.next.type = mode; } }, /** * @method editorEvents * @desc (DON'T OVERRIDE THIS) To register blur & focus Ace Editor Events * @param {aceEditor} editor */ editorEvents(editor) { const self = this; // Set schema value onBlur event editor editor.on('blur', function () { // eslint-disable-next-line no-useless-call self.setSubmitValue.call(self, editor); }); // When editor is on focus editor.on('focus', function () { // Remove Options Container if options container is on show class if (self.optionsClick) { self.optionsClick = false; } }); } } };