<iframe id="editorFrame" src="blank.html"></iframe>
We can use about: blank to get an absolutely empty page, without elements inside the body, but I preferred to create my own “empty” page, as this will allow us to start working with an empty paragraph in the body. <title></title> <body><p></p></body>
This is preferable, since Mozilla starts typing text into an empty p, like all other browsers. If this is not done, she enters the text directly into the body. Using the contentEditable property we can do without a frame, but Firefox 2 does not support contentEditable, so it’s best to still use iFrame. ( Translator's note: FF2, to put it mildly, is not relevant. So, I think, iframe is no longer needed. ) function createEditor() { var editFrame = document.getElementById("editorFrame"); editFrame.contentWindow.document.designMode="on"; } bindEvent(window, "load", createEditor);
bindEvent is a function responsible for binding a function to an event (defined in util.js). Frames, like jQuery, have corresponding functionality that you will most likely prefer to use. The next step is to create a control panel with minimal formatting functions. function Command(command, editDoc) { this.execute = function() { editDoc.execCommand(command, false, null); }; this.queryState = function() { return editDoc.queryCommandState(command) }; }
Why do we need a wrapper at all? Since we want our non-standard commands to have the same interface as the standard ones.Our button is just a span:
<span id="boldButton">Bold</span>
The span is associated with the command object through the controller: function TogglCommandController(command, elem) { this.updateUI = function() { var state = command.queryState(); elem.className = state?"active":""; } bindEvent(elem, "click", function(evt) { command.execute(); updateToolbar(); }); }
A code that was responsible for maintaining the focus of the editing window when you clicked on the button on the control panel was dropped from the listing. Below we call the ToggleCommandController function to synchronize the state of the button and draw the text, taking into account their two states. When a button is pressed, the command is executed. When an updateUI event is raised, the span receives the “active” class, or loses it, depending on the state of the text. CSS properties that determine the appearance of the button:The components are related as follows:.toolbar span { border: outset; } .toolbar span.active { border: inset; }
var command = Command("Bold", editDoc); var elem = document.getElementById(îboldButton); var controller = new TogglCommandController(command, elem); updateListeners.push(controller);
The updateListeners collection contains controllers for the control panel. The updateToolbar function iterates the list and calls the updateUI method for each controller so that all controls are exactly up to date. We attach events so that the updateToolbar is called every time the document selection changes: bindEvent(editDoc, "keyup", updateToolbar); bindEvent(editDoc, "mouseup", updateToolbar);
Just as shown above, updateToolbar is called when a command is executed. Why do we update the entire control panel after executing each command, instead of updating only the button associated with the command? Since the state of other controls as a result of the command may also change. For example, if we use the right alignment command, the state of the left alignment button and the centering button will also change. Instead of keeping track of all possible dependencies, it is easier to update the entire control panel. Now we have a basic interface for commands with two states. Using the resulting framework, the Bold, Italic, JustifyLeft, JustifyRight, and JustifyCenter commands are implemented. function LinkCommand(editDoc) { var tagFilter = function(elem){ return elem.tagName=="A"; }; //(1) this.execute = function() { var a = getContaining(editWindow, tagFilter); //(2) var initialUrl = a ? a.href : "http://"; //(3) var url = window.prompt("Enter an URL:", initialUrl); if (url===null) return; //(4) if (url==="") { editDoc.execCommand("unlink", false, null); //(5) } else { editDoc.execCommand("createLink", false, url); //(6) } }; this.queryState = function() { return !!getContaining(editWindow, tagFilter); //(7) }; }
The logic of the function is as follows: var getContaining = (window.getSelection)?w3_getContaining:ie_getContaining;
The implementation of the function in IE is more interesting, since it shows some features of the selection API in IE. function ie_getContaining(editWindow, filter) { var selection = editWindow.document.selection; if (selection.type=="Control") { //(1) // control selection var range = selection.createRange(); if (range.length==1) { var elem = range.item(0); //(3) } else { // multiple control selection return null; //(2) } } else { var range = selection.createRange(); //(4) var elem = range.parentElement(); } return getAncestor(elem, filter); }
It works like this: function w3_getContaining(editWindow, filter) { var range = editWindow.getSelection().getRangeAt(0); //(1) var container = range.commonAncestorContainer; //(2) return getAncestor(container, filter); }
It works like this: /* walks up the hierachy until an element with the tagName if found. Returns null if no element is found before BODY */ function getAncestor(elem, filter) { while (elem.tagName!="BODY") { if (filter(elem)) return elem; elem = elem.parentNode; } return null; }
<select id="fontSelector"> <option value="">Default</option> <option value="Courier">Courier</option> <option value="Verdana">Verdana</option> <option value="Georgia">Georgia</option> </select>
The command object is still simple, as it is a superstructure of the standard FontName command: function ValueCommand(command, editDoc) { this.execute = function(value) { editDoc.execCommand(command, false, value); }; this.queryValue = function() { return editDoc.queryCommandValue(command) }; }
The difference between ValueCommand and the previously described commands with binary states is the presence of the queryValue method, which returns the current value as a string. The controller executes the command when the user selects a value in the drop-down list. function ValueSelectorController(command, elem) { this.updateUI = function() { var value = command.queryValue(); elem.value = value; } bindEvent(elem, "change", function(evt) { editWindow.focus(); command.execute(elem.value); updateToolbar(); }); }
The controller is quite simple, since we use the values ​​in the drop-down list directly as values ​​for the command. The drop-down list of font sizes works in the same way - we use the FontSize built-in command and use sizes from 1 to 7 as available values. function HelloWorldCommand() { this.execute = function() { var elem = editWindow.document.createElement("SPAN"); elem.style.backgroundColor = "red"; elem.innerHTML = "Hello world!"; overwriteWithNode(elem); } this.queryState = function() { return false; } }
The chip in the overwriteWithNode function, which inserts an element at the current entry point. (The name of the method indicates that if there is a non-empty selection, its contents will be overwritten). Due to the differences in DOM between IE and browsers that support the DOM Range standard, the method is applied differently. Let's first consider the version working with the DOM Range: function w3_overwriteWithNode(node) { var rng = editWindow.getSelection().getRangeAt(0); rng.deleteContents(); if (isTextNode(rng.startContainer)) { var refNode = rightPart(rng.startContainer, rng.startOffset) refNode.parentNode.insertBefore(node, refNode); } else { var refNode = rng.startContainer.childNodes[rng.startOffset]; rng.startContainer.insertBefore(node, refNode); } }
range.deleteContents, according to its name, deletes the contents of the selection, if it is not degenerate. (If the selection is degenerate, then it simply does nothing). The DOM Range object has properties that allow us to define an input point in the DOM: startContainer is the node that contains the input point and startOffset is the number that indicates the position of the input point in the parent node. For example, if startContainer is an element and startOffset is three, then the input point is between the third and fourth descendant of the element. If startContainer is a text node, then startOffset means the offset in characters from the beginning of the parent. For example, startOffset equal to 3 means that the input point is between the third and fourth characters.endContainer and endOffset in the same way indicate the end of the selection. If the selection is empty (degenerate), then they have the same value as that of startContainer and startOffset.
function ie_overwriteWithNode(node) { var range = editWindow.document.selection.createRange(); var marker = writeMarkerNode(range); marker.appendChild(node); marker.removeNode(); // removes node but not children } // writes a marker node on a range and returns the node. function writeMarkerNode(range) { var id = editWindow.document.uniqueID; var html = "<span id='" + id + "'></span>"; range.pasteHTML(html); var node = editWindow.document.getElementById(id); return node; }
Pay attention to remove the marker node after it is finished. This is necessary not to clog the HTML code. Now we have a command that inserts arbitrary HTML into the selection point. We used the button on the control panel and the ToggleCommandController function to associate this action with the user interface.Source: https://habr.com/ru/post/107907/
All Articles