Quick little document to outline changes to the extension architecture, and internal tooling. NOTE i'm not 100% married to any of these ideas in particular, when @davi brought up "stepping back" and rethinking architecture I sat down with a bottle of bourbon and started typing. This is sorta-kinda what I would envision an API looking like (minus a few caveats of trying to retain something reselmbling a migration path forward). This is mostly just a braindump.
// this needs to work, just like this. always.
var editor = new MediumEditor(".elements", {
// optional options / default overrides
});
Buttons defined the same:
new MediumEditor(".editor", {
buttons:["bold","italic","underline"]
});
All the things are considered "extensions". The "extension" is minimal, it just defines the way the editor interacts with the extension. The above 3-button example would instantiate a new Button() passing in each of the definitions for bold
, italic
, and underline
.
// extensions "can" inherit from root Extension, this is just the API def
var MyExtension = MediumEditor.Extension.extend({
// explicit name override. defaults to the key in the `extensions`
// list, but allows for overriding here to allow an extension to
// take control over a default builtin button/etc
name: "",
init: function(){
// called as part of ctor. indicates the `this.base` is _going_
// to use the extension. adjust props and whatnot. `setup` will
// be called when activating
},
setup: function(){
// this.base exists, this.base.options exist
// we are being told we are wanted as an extension to `this.base`
// and that we should register ourselves with things and do our work
},
destroy: function(){
// we are expected to cleanup any event listeners and things.
// basically promise to undo anything we did during init phase
}
});
Placeholders
becomes an extensions. All it does is placeholders. It is 'registered' as an extension.
An idea as to how to better instantiate/define overrides/props:
var CustomExtension = MediumEditor.Button.extend({
label:"<i class='icon-warning-sign'></i>",
handleClick: function(e){
// my button was clicked
}
});
new MediumEditor(".editor", {
buttons:["anchor","custom-button"],
options:{
toolbar: false,
placeholders: false,
anchor: {
targetBlank: false,
target: false,
anchorButton: false,
anchorButtonClass: 'btn'
anchorInputPlaceholder: 'Paste or type a link',
anchorInputCheckboxLabel: 'Open in new window',
},
"anchor-preview":{
anchorPreviewHideDelay: 500,
},
"custom-button": new CusomExtension({
// my options
})
}
});
Note, the Default version of the above would look like:
MediumEditor.prototype.defaults.extensions = {
// the default options for each of these are defined as
// proto props (or a special .options? meh, I like mix(this, options)
"toolbar": true,
"placeholder": true,
"paste": true,
"anchor-preview": true
}
The possible values passed to extensions
object are:
- true: enabled, no override options
- false/undefined/null: disabled, does not instantiate.
- object: passed as overrides to existing ctor? or as init props? replaces.
- function: replaces/creates default ctor
So the above defaults means: a Toolbar, PasteHandler, Placeholders and AnchorPreview are all created with default props.
So we end up with a namespace like:
MediumEditor = function(elements, options){
this.elements = elements;
copyInto(this, options);
this.init.apply(this, arguments);
};
MediumEditor.version = "5.0.0";
MediumEditor.defaults = {
// existing option defaults?
buttons:[],
options:{
"toolbar": true // etc
}
};
// exposed utility and management stuff?
MediumEditor.util = Util;
MediumEditor.selection = Selection;
MediumEditor.buttons = {
// buttonsData ... k/v pairs of "builtin" button definitions.
// used internally to auto-instantiate Button extensions with
// known property defaults
}
MediumEditor.Extension = function(opts){
// same mixin pattern as core editor. opts override defaults,
// default defined as instance/proto props
copyInto(this, opts);
this.init.apply(this, arguments);
}
MediumEditor.Extension.prototype = {
// core extension api. setup/init/destroy/et al?
}
// allow people to extend whatever
MediumEditor.Extension.extend = Util.extendify;
// the list of default loaded extensions. users can push directly here
// prior to initialization to mindlessly override. Each extension is a
// constructor here, not instantiated. The editor will `new` it up
// when it determines it is needed for an instance. This is just a mapping
// used by statup/init cycle to determine which ctor to use for a particular
// extension. When init happens, and the list of overrides and definitions
// needed is determined we instantiate these extensions and decorate them
// with a `.base` property.
MediumEditor.extensions = {
// exposing the toolbar as an extension just allows someone to
// inject a custom brand of the toolbar in stock overrides
toolbar: Toolbar,
// already kind-of extensions.
placeholders: Placeholders,
paste: PasteHandler,
// pseudo ideas, names probably bad:
images: ImageManager // break any image logic out? overlaps with drag?
"drag-and-drop": DragManager // replace core usage with extension?
// these are all "Button" subclass/extensions. replace in the same manner
"anchor": AnchorExtension,
"anchor-preview": AnchorPreview,
// not in yet:
"font-size": FontSizeExtension,
"font-color": FontColorExtension
};
MediumEditor.prototype = {
init: function(){
// magic happens. determine which extensions need setup.
// the list of things to call .init on are some funky combination
// of default extensions -> +/- override config values -> custom
// extensions.
}
// + existing public APIs
};
Might be helpful to peek at all the existing runtime overrides available (broken into sort-of-proposed structure)
// would make more sense to implement a set("disabled", bool) ?
disableEditing: false,
allowMultiParagraphSelection: true,
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
// might consider implications of this, and provide a way away from
// fa-bulk defaults?
buttonLabels: false,
disableReturn: false,
disableDoubleReturn: false,
elementsContainer: false,
standardizeSelectionStart: false,
contentWindow: window,
ownerDocument: document,
firstHeader: 'h3',
secondHeader: 'h4',
... seems pretty straightforward?
anchorInputPlaceholder: 'Paste or type a link',
anchorInputCheckboxLabel: 'Open in new window',
checkLinkFormat: false,
targetBlank: false,
anchorTarget: false,
anchorButton: false,
anchorButtonClass: 'btn',
all of those seem like perfect candidates for AnchorExtension.prototype
// anchor-preview
anchorPreviewHideDelay: 500,
disableAnchorPreview: false,
The disabled option would be deprecated. instead, during init they'd pass
false
to the anchor-preview
extension:
new Editor(".a", { extensions: { "anchor-preview": false } });
Overwriting the anchorPreviewHideDelay
would be:
new Editor(".a", { extensions: {
"anchor-preview": {
"hideDelay": 1500
}
} });
would detect disableAnchorPreview===true
and deprecate-warn, and remove default "anchor-preview" extension from being activated
cleanPastedHTML: false,
forcePlainText: true,
these might still be core options really. but if paste
is built out as
an extension and exposed outright, any paste related-options could be defined on the "paste" override:
new Editor(".thing", {
extensions:{
"paste": {
// weirdness here. if you "paste":false, it would make the extension
// not load at all, so forcePlainText et all would not even be
// considered? this extension seems to need to be on all the time
// anyway?
clean: true,
forcePlainText: false,
omitTags:["b","h3","pre"]
}
}
});
disableToolbar: false, // -> deprecated
toolbarAlign: 'center', // -> `align`
delay: 0,
diffLeft: 0,
diffTop: -10,
activeButtonClass: 'medium-editor-button-active',
firstButtonClass: 'medium-editor-button-first',
lastButtonClass: 'medium-editor-button-last'
disablePlaceholders: false, // does this disable the palceholder in anchor?
placeholder: 'Type your text',
disablePlaceholders deprecated, pass false
to "placeholder" extension.
can do deprecation version easily. placeholder moved to property of the placeholders extension
imageDragging: true,
So the existing defaults become:
{
extensions: {
"image":{
"dragging": true
},
"placeholders":{
placeholder: "Type your text"
},
"toolbar":{
align: 'center', // -> `align`
delay: 0,
diffLeft: 0,
diffTop: -10,
activeButtonClass: 'medium-editor-button-active',
firstButtonClass: 'medium-editor-button-first',
lastButtonClass: 'medium-editor-button-last'
},
"paste":{
clean: false,
plainText: true
},
"anchor":{
inputPlaceholder: 'Paste or type a link',
inputCheckboxLabel: 'Open in new window',
checkLinkFormat: false,
targetBlank: false,
anchorTarget: false,
anchorButton: false,
anchorbuttonClass: 'btn'
},
"anchor-preview":{
iideDelay: 500
}
},
allowMultiParagraphSelection: true,
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
// might consider implications of this, and provide a way away from
// fa-bulk defaults?
buttonLabels: false,
disableReturn: false,
disableDoubleReturn: false,
elementsContainer: false,
standardizeSelectionStart: false,
contentWindow: window,
ownerDocument: document,
firstHeader: 'h3',
secondHeader: 'h4'
}
Also, MediumEditor always has been pure javascript, with no external libs, but maybe its time to rethink it aswell. I already mentioned to Nate and Noah that maybe we should consider using Rangy for the selection part, for example.
Another thing is to have a more structured internal event system (or pub/sub thing).
With all that in mind, there is only one thing that I really would like to keep: it should be easy to contribute to the project.