medium-editor
Version:
Medium.com WYSIWYG editor clone.
273 lines (240 loc) • 10.2 kB
JavaScript
(function () {
'use strict';
var Extension = function (options) {
MediumEditor.util.extend(this, options);
};
Extension.extend = function (protoProps) {
// magic extender thinger. mostly borrowed from backbone/goog.inherits
// place this function on some thing you want extend-able.
//
// example:
//
// function Thing(args){
// this.options = args;
// }
//
// Thing.prototype = { foo: "bar" };
// Thing.extend = extenderify;
//
// var ThingTwo = Thing.extend({ foo: "baz" });
//
// var thingOne = new Thing(); // foo === "bar"
// var thingTwo = new ThingTwo(); // foo === "baz"
//
// which seems like some simply shallow copy nonsense
// at first, but a lot more is going on there.
//
// passing a `constructor` to the extend props
// will cause the instance to instantiate through that
// instead of the parent's constructor.
var parent = this,
child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function () {
return parent.apply(this, arguments);
};
}
// das statics (.extend comes over, so your subclass can have subclasses too)
MediumEditor.util.extend(child, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
var Surrogate = function () {
this.constructor = child;
};
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate();
if (protoProps) {
MediumEditor.util.extend(child.prototype, protoProps);
}
// todo: $super?
return child;
};
Extension.prototype = {
/* init: [function]
*
* Called by MediumEditor during initialization.
* The .base property will already have been set to
* current instance of MediumEditor when this is called.
* All helper methods will exist as well
*/
init: function () {},
/* base: [MediumEditor instance]
*
* If not overriden, this will be set to the current instance
* of MediumEditor, before the init method is called
*/
base: undefined,
/* name: [string]
*
* 'name' of the extension, used for retrieving the extension.
* If not set, MediumEditor will set this to be the key
* used when passing the extension into MediumEditor via the
* 'extensions' option
*/
name: undefined,
/* checkState: [function (node)]
*
* If implemented, this function will be called one or more times
* the state of the editor & toolbar are updated.
* When the state is updated, the editor does the following:
*
* 1) Find the parent node containing the current selection
* 2) Call checkState on the extension, passing the node as an argument
* 3) Get the parent node of the previous node
* 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
*/
checkState: undefined,
/* destroy: [function ()]
*
* This method should remove any created html, custom event handlers
* or any other cleanup tasks that should be performed.
* If implemented, this function will be called when MediumEditor's
* destroy method has been called.
*/
destroy: undefined,
/* As alternatives to checkState, these functions provide a more structured
* path to updating the state of an extension (usually a button) whenever
* the state of the editor & toolbar are updated.
*/
/* queryCommandState: [function ()]
*
* If implemented, this function will be called once on each extension
* when the state of the editor/toolbar is being updated.
*
* If this function returns a non-null value, the extension will
* be ignored as the code climbs the dom tree.
*
* If this function returns true, and the setActive() function is defined
* setActive() will be called
*/
queryCommandState: undefined,
/* isActive: [function ()]
*
* If implemented, this function will be called when MediumEditor
* has determined that this extension is 'active' for the current selection.
* This may be called when the editor & toolbar are being updated,
* but only if queryCommandState() or isAlreadyApplied() functions
* are implemented, and when called, return true.
*/
isActive: undefined,
/* isAlreadyApplied: [function (node)]
*
* If implemented, this function is similar to checkState() in
* that it will be called repeatedly as MediumEditor moves up
* the DOM to update the editor & toolbar after a state change.
*
* NOTE: This function will NOT be called if checkState() has
* been implemented. This function will NOT be called if
* queryCommandState() is implemented and returns a non-null
* value when called
*/
isAlreadyApplied: undefined,
/* setActive: [function ()]
*
* If implemented, this function is called when MediumEditor knows
* that this extension is currently enabled. Currently, this
* function is called when updating the editor & toolbar, and
* only if queryCommandState() or isAlreadyApplied(node) return
* true when called
*/
setActive: undefined,
/* setInactive: [function ()]
*
* If implemented, this function is called when MediumEditor knows
* that this extension is currently disabled. Curently, this
* is called at the beginning of each state change for
* the editor & toolbar. After calling this, MediumEditor
* will attempt to update the extension, either via checkState()
* or the combination of queryCommandState(), isAlreadyApplied(node),
* isActive(), and setActive()
*/
setInactive: undefined,
/* getInteractionElements: [function ()]
*
* If the extension renders any elements that the user can interact with,
* this method should be implemented and return the root element or an array
* containing all of the root elements. MediumEditor will call this function
* during interaction to see if the user clicked on something outside of the editor.
* The elements are used to check if the target element of a click or
* other user event is a descendant of any extension elements.
* This way, the editor can also count user interaction within editor elements as
* interactions with the editor, and thus not trigger 'blur'
*/
getInteractionElements: undefined,
/************************ Helpers ************************
* The following are helpers that are either set by MediumEditor
* during initialization, or are helper methods which either
* route calls to the MediumEditor instance or provide common
* functionality for all extensions
*********************************************************/
/* window: [Window]
*
* If not overriden, this will be set to the window object
* to be used by MediumEditor and its extensions. This is
* passed via the 'contentWindow' option to MediumEditor
* and is the global 'window' object by default
*/
'window': undefined,
/* document: [Document]
*
* If not overriden, this will be set to the document object
* to be used by MediumEditor and its extensions. This is
* passed via the 'ownerDocument' optin to MediumEditor
* and is the global 'document' object by default
*/
'document': undefined,
/* getEditorElements: [function ()]
*
* Helper function which returns an array containing
* all the contenteditable elements for this instance
* of MediumEditor
*/
getEditorElements: function () {
return this.base.elements;
},
/* getEditorId: [function ()]
*
* Helper function which returns a unique identifier
* for this instance of MediumEditor
*/
getEditorId: function () {
return this.base.id;
},
/* getEditorOptions: [function (option)]
*
* Helper function which returns the value of an option
* used to initialize this instance of MediumEditor
*/
getEditorOption: function (option) {
return this.base.options[option];
}
};
/* List of method names to add to the prototype of Extension
* Each of these methods will be defined as helpers that
* just call directly into the MediumEditor instance.
*
* example for 'on' method:
* Extension.prototype.on = function () {
* return this.base.on.apply(this.base, arguments);
* }
*/
[
// general helpers
'execAction',
// event handling
'on',
'off',
'subscribe',
'trigger'
].forEach(function (helper) {
Extension.prototype[helper] = function () {
return this.base[helper].apply(this.base, arguments);
};
});
MediumEditor.Extension = Extension;
})();