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'
}
is how this all started, really. @davi suggested stepping back and rethinking how options and .base decoration happens, so I spewed out the above. most of the above could be introduced without breaking back compat (with deprecation warnings/etc), but some is an outright shift, and no sense in preserving a broken pattern if it is in fact deemed broken and wanting replaced.
ultimately the nested options are just moving properties from one place to a newly named place. it is a little fuzzy here. the whole nested-options thing came out of @davi mentioning specifically "the way options were passed in" ... I immediately noticed the inconsistency in how various internal-things pass instance and options ... then fell back to the pattern of: each extensions 'options' are just prototype properties, and there are no explicit
options
to speak of. the "options" being passed to the constructor is justcopyInto(this, options)
. so ...So the above extension has two configuration options, one defaults true the other false.
revolves around there being no options per-se being passed via ctor, just potential overrides (making them one in the same). The weirdest bit would be discovery between the
magic
anddifferentmagic
patterns, where on is a ctor and the other already-instantiated?I think moving the toolbar-specific options to Toolbar.prototype overrides, and exposing as an extension is "enough" for first pass. Discovering how people want to use it will only come after they are able to use it. I have my own desires for a replaceable toolbar, and would be happy to share :)
All of the option-moving can be done as s deprecation pass ... though the use of an explicit
options
passed to any of the extensions would be a breaking change, but the ability to do exactly the same by mixing over your extension .proto (or inheriting from Extension directly instead of making your own ctor) is a pretty easy migration path for extension authors.