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'
}
I'm definitely on board with these suggestions, I think this helps out with our large pile of options, and also helps us standardize the lifecycle of our built-in extensions. This is a great end-goal for us, I'd like to see what Davi and Noah think.
Ultimately, I think this would be 4 separate sets of changes:
destroy
andsetup
methods instead ofactivate
anddeactivate
for the extensions. I'm not sure if we'll need to "deprecate" vs "delete" them or not, as the introduction of these methods on extension themselves was only added recently.