User:Conrad.Irwin/editor docs

From Wiktionary, the free dictionary
Jump to navigation Jump to search
If you want to make the editor do new things to the page, look at the very exciting Basic example

This page is intended for those wishing to help develop editor.js, port it to other places, or just understand what on earth it does. Experience has shown that this document will quickly fall out of date, but it is hoped that it will remain useful even then.

If you are planning to dig into the code, I strongly recommend you stick within one local bit of the code, and just trust that the rest works as documented. Trying to bend your head around the flow-paths is unlikely to be successful.

Introduction[edit]

The coding style is, by necessity, strongly reliant on continuation functions. The editor must do a reasonable amount of MediaWiki API work, and javascript has no native support for freezing execution (except in function sized blocks). If you are not familiar with callbacks, Continuation passing on Wikipedia may help you get started.

The main complexity of this comes from trying to edit the HTML of the page and the Wikitext synchronously, so that all changes to the page accurately reflect the underlying wikitext. It matters less if there is a bug in the code if the user can see the bug before clicking save and report it themselves.

The code has been divided into several large chunks, in the hope that the basics can be re-used, and feature-interaction can be kept to a minimum. At the moment, in addition to the code in this file, it depends on the following functions from MediaWiki:Common.js.

getCookie(cookiename); // only in CookiePreferences
setCookie(cookiename, value); // only in CookiePreferences
newNode(...); //pervasive

Components[edit]

JsMwApi[edit]

The network-work is done by JsMwApi which has its own extensive documentation at User_talk:Conrad.Irwin/Api.js. I will not repeat that here, except to point out that

var api = JsMwApi(); //returns a new api object.
/*void*/ api(query, continuation); //calls the continuate with the result.

CookiePreferences[edit]

This library depends on getCookie(name) and setCookie(name, value). Its purpose is simply to hold string preferences. While it could be generalised to serialise any object, storing the preferences as strings makes it clearer as to what is going on.

Each browser "cookie" holds several key-value pairs, so seperate projects can use the same library providing they use a unique context name. It also has the useful property that users who do not override the default preferences do not need to store anything.

Interface[edit]

var store = new CookiePreferences(context);

The context should ideally be a unique name for your project, for example, editor.js uses "EditorJs" as its name.

var value = store.get(name, default value);

Gets the value that was previously stored under this name. If no value was previously stored, it returns the default value. This prevents the need for having to register each preference with the store in advance.

/*void*/ store.set(name, value);

Ensures that next time get is called with name, the value will be returned. The length of time that this will be stored for depends on the definition of setCookie()

Usage in editor.js[edit]

There are currently two contexts in use by editor.js:

EditorJs
The main set of preferences is here, the settings here are set manually by clicking on divs on Wiktionary with specific ids ("editor-js-disable-button" and "editor-js-labeller-button"). These are handled in the addOnloadHook function. It stores:
enabled ("true" or "false)
Whether to enable any features at all.
labeller ("true" or "false")
Whether the TranslationLabeller should be created for each translation table.
TranslationAdder
Stores little bits of information about how the user uses the TranslationAdder monster. These preferences are handled silently and really exist just to make this more pleasant to use.
more-display ("none" or "block")
Is the "More" button open or closed by default.
curlang (langcode)
The last used language code (so that we can prefil the form for them)
script-langcode (scriptcode)
Stores any manually overridden script guesses (see below)
comma-knowledge("none" or "guru")
By default we prevent users adding translations with commas, but if they've clicked through this once before, we let them through automatically (though still display a warning)

Editor[edit]

This is the main heart of the operation, things that it uses are tools, and things that use it are addons. It should be possible to use this class to make any kind of edit to the current page (it does not support editing other pages, and probably never will, using JsMwApi directly will be easier).

The central idea here is that you provide a sequence of "edits" (through the addEdit function) and it provides you with minimal undo, redo, and save facilities. While "undo" and "redo" are not trivial to implement, and require additional complexity in everything, they are essential for the many cases where there is no converse edit (i.e. you can add a translation, but you can't delete one - only "undo" the add).

I briefly toyed around with the idea of making two copies of the DOM so that undo and redo would be much simpler, however this leads to a lot of (un)expected interaction with other javascript tools and a reasonably severe loss of stability on many pages.

Each "edit" can be thought of as a transaction. It should either happen completely and correctly or not happen at all. At all stages the HTML should reflect what the wikitext looks like. If you are writing code for a wiki very different from en.wikt, this may be all you need to know, it is very likely simpler to delete the vast amount of wiki-specific code that actually interacts with this object.

Interface[edit]

var editor = new Editor();

The editor is actually a singleton, so you can call this as and when it looks comfortable to do so.

var page = editor.page;

This is the underlying JsMwApi page object that represents the current page. It can be useful if you want to run custom API calls with the right context (particularly the parse and parseFragment methods are useful).

/*void*/ editor.withCurrentText(continuation);

If you want to do anything with the current page's wikitext, which may or may not have been edited yet, then this is the method to call. It calls your continuation function with one argument, the current page text. This is not for making edits, you should use addEdit if you want to modify the wikitext, but it can be useful for looking up default values.

/*void*/ editor.addEdit(edit object, containing node);

The edit object is described in more detail below, the containing node will probably be refactored into the edit object. It is a node that will be outlined (in an unignorable green) until the page is saved so that the user can see what change they made. The edit object itself completely describes one transaction. Calling this function will also cause the editor interface to appear in the top-left. It flashes briefly so that people can spot it, and then stays rather unassumingly in the top left.

/*void*/ editor.error(message);

Displays an error message to the user (not that there are ever any errors or anything). They appear on a red background just below the save button. This should not be used for validation errors, which can be caught and shown to the user near to where they were editing (without even opening the save buttons) but if something goes awry asynchronously with the user, such as unexpected wikitext, then this can be used.

/*void*/ editor.undo();
/*void*/ editor.redo();

As though the user had clicked on the "undo" or "redo" buttons. This isn't very useful, though it is used when the user has indicated that they know not to add comma-seperated lists of translations, when they create a translation with a comma an undo button is provided in the warning message.

Edit objects[edit]

An edit object represents one "edit" to the wikitext, it also contains all the information to make changes to the HTML and create an edit summary. Once you've handed an edit to an Editor() it is there forever, you shouldn't modify it at all because you don't know easily what state it is in. The edit should not depend on the state of the DOM or wikitext at the time of calling addEdit, instead it should use the current state when its method's are called (while they are likely to be similar, it is possible that they will change as the browser may be loading the page before being able to apply previous edits).

The edit object should have the following properties

edit.edit = function (wikitext) { return wikitext or null;}

This is the function that edits the page's wikitext. You can use closures to store as much other information inside the function as you need. As this must return a value, it cannot wait for any functions that rely on continuation passing - this is by design. If this function returns null, or the exact input, it is deemed to have failed, and the corresponding undo and redo methods will not be called. If it raises an exception, Editor().error() is called to display it to the user, and again, undo nor redo will be called.

edit.redo = function () { return /*void*/; }

This is the function that edits the HTML (it is called redo even though it will be first called before undo and so should maybe be called do but that is confusing too). Making sure that this function always makes the same change as edit is hard, context should be checked very carefully, and it is better to abort the entire edit than to make one that is not absolutely certain to be correct.

edit.undo = function () { return /*void*/; }

This function is usually easy to write, you simply undo the change that redo makes. As any DOM parsing will have been done by redo this is normally just a matter of inserting and/or removing a node from the DOM. To give the users even nicer happy feelings, this method can also be made to refil any form fields that were used to make it.

edit.summary = text;

The text to be added to the edit summary so that we know what is happening. Try to keep this value reasonably lengthed so that four or five edits will still not have their summary truncated (the translation gloss editor truncates it to length 50, which seems to be about right).

AdderWrapper[edit]

This code exists to simplify the process of:

  • add form to DOM
  • wait for user to submit form
  • validate user input
  • send a fragment of wikitext to the API to parse
  • make an edit, using the parsed wikitext in the redo function

If you don't want that control flow, then don't use this wrapper.

The interface of the adder objects is fairly simple, but slightly arbitrary. If you want an idea of how this works I suggest studying the TranslationLabellers, the original users of this interface, TranslationAdders has become even more messy!

Interface[edit]

new AdderWrapper(editor, adder object, insertNode, insertSibling)

This constructs a new wrapper, and the wrapper has no useful public functions, so it can be discarded unless needed for book-keeping. the editor is always new Editor(). The insertNode and insertSibling dictate where in the DOM you would like the <form> tag placed. If insertSibling is provided: insertNode.insertBefore(form, insertSibling), otherwise: insertNode.appendChild(form).

The adder object itself is more interesting. It has three public fields:

adder.createForm = function () { return newNode('form'); };

Return a DOM node that is a form with no onsubmit handlers that may get crashed into by the AdderWrapper.

adder.fields = {name: validator or "checkbox"}

For each text input field in the form, you should provide a validation function, any checkboxes must be documented as name:"checkbox". other types of input are not yet supported. This array documents what you expect from the user when they submit the form.

validator = function(value, onerror) {return value or onerror(errormessage); }

The validator functions allow you to sanitize the value. If you want to reject the users input, simply return onerror("input was invalid because:"). If you want to accept it, then return the value provided; a nice middle ground exists where if you want to clean what they put in, you can return a cleaned value - this can be used to convert language names to codes, or to fix other common mistakes. util.validateNoWikiText(fieldname) exists and returns a function that ensures there are no []{} characters in the text and provides a sensible error messsage if there are.

adder.onsubmit =  function(values, render());

If the input passes all the validations specified by .fields, the onsubmit method will be called with the sanitized values returned by the validator in an object. This allows you to construct the actual edit from the users input. The render function is provided to parse a short fragment of wikitext so that the HTML may be edited correctly - typically it is only in the callback that render calls that eventually makes the actual edit.

/*void*/ render(wikitext, continuation);

This render function uses the JsMwApi parseFragment method to parse a small amount of wikitext (not more than one line, maybe much less). It then calls the continuation with only the HTML fragment that results. This can be clearly seen by looking at the TranslationLabeller code. This continuation function should be used to call addEdit.

Util[edit]

To document (mainly functions to edit translation tables on en.wikt)

TranslationAdders[edit]

To document (heeelp!)

TranslationBalancers[edit]

To document (just those two simple buttons, how hard could it be)

TranslationLabellers[edit]

To document (the little, optional, functionality)