Skip to content

Instantly share code, notes, and snippets.

@phiggins42
Last active August 29, 2015 14:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phiggins42/a9dd79afb4fc6feb2ec1 to your computer and use it in GitHub Desktop.
Save phiggins42/a9dd79afb4fc6feb2ec1 to your computer and use it in GitHub Desktop.

Architecture Shift

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.

overall

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)

Core

// 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?

anchor

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

// 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

paste

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"]
        }
    }
});

toolbar

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'

placeholders

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

image? drag-and-drop?

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'
}
@nchase
Copy link

nchase commented Apr 3, 2015

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment