medium-editor
Version:
Medium.com WYSIWYG editor clone.
236 lines (200 loc) • 8.65 kB
JavaScript
(function () {
'use strict';
var Button = MediumEditor.Extension.extend({
/* Button Options */
/* action: [string]
* The action argument to pass to MediumEditor.execAction()
* when the button is clicked
*/
action: undefined,
/* aria: [string]
* The value to add as the aria-label attribute of the button
* element displayed in the toolbar.
* This is also used as the tooltip for the button
*/
aria: undefined,
/* tagNames: [Array]
* NOTE: This is not used if useQueryState is set to true.
*
* Array of element tag names that would indicate that this
* button has already been applied. If this action has already
* been applied, the button will be displayed as 'active' in the toolbar
*
* Example:
* For 'bold', if the text is ever within a <b> or <strong>
* tag that indicates the text is already bold. So the array
* of tagNames for bold would be: ['b', 'strong']
*/
tagNames: undefined,
/* style: [Object]
* NOTE: This is not used if useQueryState is set to true.
*
* A pair of css property & value(s) that indicate that this
* button has already been applied. If this action has already
* been applied, the button will be displayed as 'active' in the toolbar
* Properties of the object:
* prop [String]: name of the css property
* value [String]: value(s) of the css property
* multiple values can be separated by a '|'
*
* Example:
* For 'bold', if the text is ever within an element with a 'font-weight'
* style property set to '700' or 'bold', that indicates the text
* is already bold. So the style object for bold would be:
* { prop: 'font-weight', value: '700|bold' }
*/
style: undefined,
/* useQueryState: [boolean]
* Enables/disables whether this button should use the built-in
* document.queryCommandState() method to determine whether
* the action has already been applied. If the action has already
* been applied, the button will be displayed as 'active' in the toolbar
*
* Example:
* For 'bold', if this is set to true, the code will call:
* document.queryCommandState('bold') which will return true if the
* browser thinks the text is already bold, and false otherwise
*/
useQueryState: undefined,
/* contentDefault: [string]
* Default innerHTML to put inside the button
*/
contentDefault: undefined,
/* contentFA: [string]
* The innerHTML to use for the content of the button
* if the `buttonLabels` option for MediumEditor is set to 'fontawesome'
*/
contentFA: undefined,
/* classList: [Array]
* An array of classNames (strings) to be added to the button
*/
classList: undefined,
/* attrs: [object]
* A set of key-value pairs to add to the button as custom attributes
*/
attrs: undefined,
// The button constructor can optionally accept the name of a built-in button
// (ie 'bold', 'italic', etc.)
// When the name of a button is passed, it will initialize itself with the
// configuration for that button
constructor: function (options) {
if (Button.isBuiltInButton(options)) {
MediumEditor.Extension.call(this, this.defaults[options]);
} else {
MediumEditor.Extension.call(this, options);
}
},
init: function () {
MediumEditor.Extension.prototype.init.apply(this, arguments);
this.button = this.createButton();
this.on(this.button, 'click', this.handleClick.bind(this));
},
/* getButton: [function ()]
*
* If implemented, this function will be called when
* the toolbar is being created. The DOM Element returned
* by this function will be appended to the toolbar along
* with any other buttons.
*/
getButton: function () {
return this.button;
},
getAction: function () {
return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;
},
getAria: function () {
return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;
},
getTagNames: function () {
return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;
},
createButton: function () {
var button = this.document.createElement('button'),
content = this.contentDefault,
ariaLabel = this.getAria(),
buttonLabels = this.getEditorOption('buttonLabels');
// Add class names
button.classList.add('medium-editor-action');
button.classList.add('medium-editor-action-' + this.name);
if (this.classList) {
this.classList.forEach(function (className) {
button.classList.add(className);
});
}
// Add attributes
button.setAttribute('data-action', this.getAction());
if (ariaLabel) {
button.setAttribute('title', ariaLabel);
button.setAttribute('aria-label', ariaLabel);
}
if (this.attrs) {
Object.keys(this.attrs).forEach(function (attr) {
button.setAttribute(attr, this.attrs[attr]);
}, this);
}
if (buttonLabels === 'fontawesome' && this.contentFA) {
content = this.contentFA;
}
button.innerHTML = content;
return button;
},
handleClick: function (event) {
event.preventDefault();
event.stopPropagation();
var action = this.getAction();
if (action) {
this.execAction(action);
}
},
isActive: function () {
return this.button.classList.contains(this.getEditorOption('activeButtonClass'));
},
setInactive: function () {
this.button.classList.remove(this.getEditorOption('activeButtonClass'));
delete this.knownState;
},
setActive: function () {
this.button.classList.add(this.getEditorOption('activeButtonClass'));
delete this.knownState;
},
queryCommandState: function () {
var queryState = null;
if (this.useQueryState) {
queryState = this.base.queryCommandState(this.getAction());
}
return queryState;
},
isAlreadyApplied: function (node) {
var isMatch = false,
tagNames = this.getTagNames(),
styleVals,
computedStyle;
if (this.knownState === false || this.knownState === true) {
return this.knownState;
}
if (tagNames && tagNames.length > 0) {
isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;
}
if (!isMatch && this.style) {
styleVals = this.style.value.split('|');
computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);
styleVals.forEach(function (val) {
if (!this.knownState) {
isMatch = (computedStyle.indexOf(val) !== -1);
// text-decoration is not inherited by default
// so if the computed style for text-decoration doesn't match
// don't write to knownState so we can fallback to other checks
if (isMatch || this.style.prop !== 'text-decoration') {
this.knownState = isMatch;
}
}
}, this);
}
return isMatch;
}
});
Button.isBuiltInButton = function (name) {
return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);
};
MediumEditor.extensions.button = Button;
}());