Let's have contenteditable div. Browser itself manage undo on it.
But when additional content changes (or touching selection ranges) are made from script (in addition to user action) then it stops behave as user expected.
In other words when user hit Ctrl+Z then div content is not reverted to previous state.
See following simplified artificial example:
https://codepen.io/farin/pen/WNEMVEB
const editor = document.getElementById("editor")
editor.addEventListener("keydown", ev => {
if (ev.key === 'a') {
const sel = window.getSelection()
const range = window.getSelection().getRangeAt(0)
const node = range.startContainer;
const value = node.nodeValue
node.nodeValue = value + 'aa'
range.setStart(node, value.length + 2)
range.setEnd(node, value.length + 2)
ev.preventDefault()
}
})
All written 'a' letters are doubled.
Undo is ok as long as there is no 'a' typed.
When user typed 'a' (appended to text as double 'aa') and hits Ctrl+Z, then he expects both 'a' will be removed and cursor moves back to original position.
Instead only one 'a' is reverted on undo and second one added by script remain.
If event is also prevented by preventDefault() (which is not needed in this example, but in my real world example i can hardly avoid it) then all is worse.
Because undo reverts previous user action.
I could images that whole undo/redo stuff will be managed by script, but it means implementation of whole undo/redo logic. That's too complicated, possible fragile and with possible many glitches.
Instead I would like tell browser something like that there is atomic change which should be reverted by one user undo. Is this possible?
You can store the "revisions" in an array, then push the innerHTML of the div to it whenever you programmatically change the innerHTML of it.
Then, you can set the innerHTML of the div to the last item in the revisions array whenever the user uses the Ctrl + Z shortcut.
const previousRevisions = []
function saveState() {
previousRevisions.push(editor.innerHTML)
}
function undoEdit() {
if (previousRevisions.length > 0) {
editor.innerHTML = previousRevisions.pop();
}
}
const editor = document.getElementById("editor")
editor.addEventListener("keydown", ev => {
if (ev.key === 'a') {
saveState()
const sel = window.getSelection()
const range = window.getSelection().getRangeAt(0)
const node = range.startContainer;
const value = node.nodeValue
node.nodeValue = value + 'a'
range.setStart(node, value.length + 1)
range.setEnd(node, value.length + 1)
} else if (ev.ctrlKey && ev.key == 'z') {
undoEdit()
}
})
#editor{width:600px;min-height:250px;border:1px solid black;font-size:24px;margin:0 auto;padding:10px;font-family:monospace;word-break:break-all}
<div id="editor" contenteditable="true">type here </div>
The benefit of this solution is that it will not conflict with the browser's native Ctrl + Z shortcut behavior.
Make the parent a div (if it isn't) and make it so it adds spans inside of it every time the user taps a so the new span and set it's id to span-keyword will have aa as the value / text. Then check if the users cursor is at the beginning of it and check if there is no other text in-front of it and the user did no other action in it. If there is no text and no other actions happened do this:
document.getElementById('span-keyword').remove();
Related
I wonder how to set the text "Highlight" of a part of text inside tlfTextField with the code?
I tried "tf.backgroundColor = 0x990000" property, but did not help.
For instance, I can change the Font Color of any contents inside Parenthesis, by this code:
private function decorate():void {
var tf:TextFormat = new TextFormat();
tf.color = 0x990000;
var startPoint:int = 0;
while (startPoint != -1) {
var n1:int = textMc.tlfText.text.indexOf("(", startPoint);
var n2:int = textMc.tlfText.text.indexOf(")", n1 + 1);
if (n1 == -1 || n2 == -1) {
return;
}
textMc.tlfText.setTextFormat(tf, n1 + 1, n2);
startPoint = n2 + 1;
}
}
So I know "tf.color = 0x990000;" will change the Font color, however, don't know how to "highlight" some text, with code, as I do inside Flash manually.
You should have probably used tlfMarkup property to set the required format to the specific part of text. The attributes you seek are backgroundColor and backgroundAlpha of the span XML element that you should wrap your selection, however it should be much more difficult should there already be spans around words when you retrieve the property from your text field.
The problem with your solution is that you don't check if the two characters are located on a single line before drawing your rectangle, also you would need to redraw such rectangles each time something happens with the textfield. The proposed approach makes use of Flash HTML renderer's capabilities to preserve the formatting, however it will require a lot of work to handle this task properly.
I'd like users to enter a code and to assist them in transcribing it I'd hope to increase the spacing between every 3rd character they type. I've seen this nicely done for credit cards having 4 character spacing. This will be for an Ionic app so the simple input box coud be replaced with a customised Ionic control.
What methods have you used for this and what works best?
Open to Angular/Ionic code samples or a related web site tutorial.
Pure CSS would be nice.
Here is an other version, without jquery, works with alphanumerical and takes a configurable separator:
Typescript:
GROUP_SEPARATOR=" ";
......
format(valString) {
if (!valString) {
return '';
}
let val = valString.toString();
const parts = val.replace(/ /g, '');
return parts.replace(/\B(?=(?:\w{3})+(?!\w))/g, this.GROUP_SEPARATOR)
};
HTML
<input [(ngModel)]="input"
style="border:1px solid black" #myBudget="ngModel" (input)="input = format(input)">
DEMO
You can add space on keyup event.
Example
$('#input').on('keyup', function(e){
var val = $(this).val();
var newval = '';
val = val.replace(/\s/g, '');
for(var i=0; i < val.length; i++) {
if(i%3 == 0 && i > 0) newval = newval.concat(' ');
newval = newval.concat(val[i]);
}
$(this).val(newval);
})
I found a simpler method based on Vija's method ... Basically we match 3 non-space chars and we remove any previously added space chars. This is needed to allow the user to update or erase any chars in the text box.
A final solution may also need to adjust the position of the cursor based on where it was prior to performing the replace.
$('#input').on('keyup', function(e){
var val = $(this).val();
var newval = val.replace(/([^ ][^ ][^ ]) */g, "\$1 ").trim();
$(this).val(newval);
})
I'm trying to create a new keyboard somehow, for educational purposes.
I've written this code using actionscript 3.I've created an input text field (named it t1) .when the user presses q button on keyboard(which has an ASCII aquals 81 ) I want the letter b to be printed out on the text field so i've written this code :
stage.addEventListener(KeyboardEvent.KEY_DOWN, pressing);
function pressing(event:KeyboardEvent):void
{
//trace(event.keyCode);
if(event.keyCode==81)
t1.replaceSelectedText("b");
}
the problem was that the method replaceSelectedText prints the tow letters on the screen(q&b) which method can i use instead?
Any help would be appreciated.
When using the replaceSelectedText method, you first need to select the text you want to replace. This can be done with the "setSelection" method. This from the adobe help website:
setSelection(beginIndex:int, endIndex:int):void
"Sets as selected the text designated by the index values of the first and last characters, which are specified with the beginIndex and endIndex parameters."
At the moment, since you don't have any text selected, it appears to just be adding the text "b" as it's replacing nothing. Therefore, you should try first selecting the "q".
Alternatively, you can just use a different method. from the adobe help website:
replaceText(beginIndex:int, endIndex:int, newText:String):void
"Replaces the range of characters that the beginIndex and endIndex parameters specify with the contents of the newText parameter."
This would cut out an extra line of code.
I haven't actually done this myself, so if that doesn't work, here's the link to the adobe help page for Text Fields: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/text/TextField.html
I think that to do what you are looking for ( replace a char when it's typed ), a KeyboardEvent.KEY_DOWN is not enough, because when that event is fired, the text is not yet changed, so any change that you did in its handler to your text field will not cancel the insertion of the current typed char. Also, using KeyboardEvent.KEY_UP ( in addition to KeyboardEvent.KEY_DOWN ) will not resolve the problem, because you can fire n times a KeyboardEvent.KEY_DOWN event with the KeyboardEvent.KEY_UP event fired once !
So, I think that the best event that can do the job is the Event.CHANGE event which is fired every time the text of your text field is changed, so you can do like this :
// is there a char to replace ?
var replace_char:Boolean = false;
// the position of the char that we want to replace
var char_position:int = -1;
var text_input:TextField = new TextField();
text_input.type = 'input';
text_input.border = true;
text_input.addEventListener(Event.CHANGE, onTextChange);
function onTextChange(e:Event):void {
if(replace_char && char_position >= 0){
text_input.replaceText(char_position, char_position + 1, 'b');
replace_char = false;
}
}
addChild(text_input);
stage.addEventListener(KeyboardEvent.KEY_DOWN, _onKeyDown);
function _onKeyDown(e:KeyboardEvent):void {
if(e.keyCode == 81) {
replace_char = true;
char_position = text_input.selectionBeginIndex;
}
}
EDIT :
To use a list of keys and their equivalents, you can use an object to stock your keys like this :
// list of all keys (chars) and their equivalents
var chars:Object = {
81: 'b', // q => b
83: 'v', // s => v
68: 'c' // d => c
// other chars
}
var char_to_replace:int = -1;
// other instructions
function onTextChange(e:Event):void {
if(replace_char && char_position >= 0 && char_to_replace >= 0){
// get the equivalent of the pressed key from chars object using : chars[key_pressed]
text_input.replaceText(char_position, char_position + 1, chars[char_to_replace]);
replace_char = false;
}
}
// other instructions
function _onKeyDown(e:KeyboardEvent):void {
if(chars[e.keyCode]) {
replace_char = true;
// save the last pressed key to get its equivalent, or save this last one directly, to replace it next
char_to_replace = e.keyCode;
char_position = text_input.selectionBeginIndex;
}
}
Hope that can help.
I'm working on a script that applies custom headings to a plain text document imported in Google Docs. The scripts works pretty much as it should. However the resulting document has a weird layout, as if random page breaks were inserted here and there. But there are no page breaks and I can't understand the reason of this layout. Checking the paragraph attributes give me no hints on what is wrong.
Here is the text BEFORE the script is applied:
https://docs.google.com/document/d/1MzFvlkG13i3rrUcz5jmmSppG4sBH6zTXr7RViwdqaIo/edit?usp=sharing
You can make a copy of the document and execute the script (from the Scripts menu, choose Apply Headings). The script applies the appropriate heading to the scene heading, name of the character, dialogue, etc.
As you can see, at the bottom of page 2 and 3 of the resulting document there is a big gap and I can't figure out why. The paragraph attributes seem ok to me...
Here is a copy of the script:
// Apply headings to sceneheadings, actions, characters, dialogues, parentheticals
// to an imported plain text film script;
function ApplyHeadings() {
var pars = DocumentApp.getActiveDocument().getBody().getParagraphs();
for(var i=0; i<pars.length; i++) {
var par = pars[i];
var partext = par.getText();
var indt = par.getIndentStart();
Logger.log(indt);
if (indt > 100 && indt < 120) {
var INT = par.findText("INT.");
var EXT = par.findText("EXT.");
if (INT != null || EXT != null) {
par.setHeading(DocumentApp.ParagraphHeading.HEADING1);
par.setAttributes(ResetAttributes());
}
else {
par.setHeading(DocumentApp.ParagraphHeading.NORMAL);
par.setAttributes(ResetAttributes());
}
}
else if (indt > 245 && indt < 260) {
par.setHeading(DocumentApp.ParagraphHeading.HEADING2);
par.setAttributes(ResetAttributes());
}
else if (indt > 170 && indt < 190) {
par.setHeading(DocumentApp.ParagraphHeading.HEADING3);
par.setAttributes(ResetAttributes());
}
else if (indt > 200 && indt < 240) {
par.setHeading(DocumentApp.ParagraphHeading.HEADING4);
par.setAttributes(ResetAttributes());
}
}
}
// Reset all the attributes to "null" apart from HEADING;
function ResetAttributes() {
var style = {};
style[DocumentApp.Attribute.STRIKETHROUGH] = null;
style[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = null;
style[DocumentApp.Attribute.INDENT_START] = null;
style[DocumentApp.Attribute.INDENT_END] = null;
style[DocumentApp.Attribute.INDENT_FIRST_LINE] = null;
style[DocumentApp.Attribute.LINE_SPACING] = null;
style[DocumentApp.Attribute.ITALIC] = null;
style[DocumentApp.Attribute.FONT_SIZE] = null;
style[DocumentApp.Attribute.FONT_FAMILY] = null;
style[DocumentApp.Attribute.BOLD] = null;
style[DocumentApp.Attribute.SPACING_BEFORE] = null;
style[DocumentApp.Attribute.SPACING_AFTER] = null;
return style;
}
A couple of screenshots to make the problem more clear.
This is page 2 of the document BEFORE the script is applied.
This is page two AFTER the script is applied. Headings are applied correctly but... Why the white space at the bottom?
Note: if you manually re-apply HEADING2 to the first paragraph of page 3 (AUDIO TV), the paragraph will jump back to fill the space at the bottom of page 2. This action, however, doesn't change any attribute in the paragraph. So why the magic happens?
Thanks a lot for your patience.
That was an interesting problem ;-)
I copied your doc, ran the script and had a surprise : nothing happened !
It took me a few minutes to realize that the copy I just made had no style defined for headings, everything was for some reason in courrier new 12pt, including the headings.
I examined the log and saw the indent values, played with that a lot to finally see that the headings were there but not changing the style.
So I went in the doc menu and set 'Use my default style and... everything looks fine, see screen capture below.
So now your question : it appears that there must be something wrong in your style definition, by "wrong" I mean something that changes more than just the font Style and size but honestly I can't see any way to guess what since I'm unable to reproduce it... Please try resetting your heading styles and re-define your default.... and tell us what happens then.
PS : here are my default heading styles : (and the url of my copy in view only :https://docs.google.com/document/d/1yP0RRCrRSsQc9zCk-sdfu5olNGDkoIrabXanII4qUG0/edit?usp=sharing )
I am writing a jquery plugin that will do a browser-style find-on-page search. I need to improve the search, but don't want to get into parsing the html quite yet.
At the moment my approach is to take an entire DOM element and all nested elements and simply run a regex find/replace for a given term. In the replace I will simply wrap a span around the matched term and use that span as my anchor to do highlighting, scrolling, etc. It is vital that no characters inside any html tags are matched.
This is as close as I have gotten:
(?<=^|>)([^><].*?)(?=<|$)
It does a very good job of capturing all characters that are not in an html tag, but I'm having trouble figuring out how to insert my search term.
Input: Any html element (this could be quite large, eg <body>)
Search Term: 1 or more characters
Replace Txt: <span class='highlight'>$1</span>
UPDATE
The following regex does what I want when I'm testing with http://gskinner.com/RegExr/...
Regex: (?<=^|>)(.*?)(SEARCH_STRING)(?=.*?<|$)
Replacement: $1<span class='highlight'>$2</span>
However I am having some trouble using it in my javascript. With the following code chrome is giving me the error "Invalid regular expression: /(?<=^|>)(.?)(Mary)(?=.?<|$)/: Invalid group".
var origText = $('#'+opt.targetElements).data('origText');
var regx = new RegExp("(?<=^|>)(.*?)(" + $this.val() + ")(?=.*?<|$)", 'gi');
$('#'+opt.targetElements).each(function() {
var text = origText.replace(regx, '$1<span class="' + opt.resultClass + '">$2</span>');
$(this).html(text);
});
It's breaking on the group (?<=^|>) - is this something clumsy or a difference in the Regex engines?
UPDATE
The reason this regex is breaking on that group is because Javascript does not support regex lookbehinds. For reference & possible solutions: http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript.
Just use jQuerys built-in text() method. It will return all the characters in a selected DOM element.
For the DOM approach (docs for the Node interface): Run over all child nodes of an element. If the child is an element node, run recursively. If it's a text node, search in the text (node.data) and if you want to highlight/change something, shorten the text of the node until the found position, and insert a highligth-span with the matched text and another text node for the rest of the text.
Example code (adjusted, origin is here):
(function iterate_node(node) {
if (node.nodeType === 3) { // Node.TEXT_NODE
var text = node.data,
pos = text.search(/any regular expression/g), //indexOf also applicable
length = 5; // or whatever you found
if (pos > -1) {
node.data = text.substr(0, pos); // split into a part before...
var rest = document.createTextNode(text.substr(pos+length)); // a part after
var highlight = document.createElement("span"); // and a part between
highlight.className = "highlight";
highlight.appendChild(document.createTextNode(text.substr(pos, length)));
node.parentNode.insertBefore(rest, node.nextSibling); // insert after
node.parentNode.insertBefore(highlight, node.nextSibling);
iterate_node(rest); // maybe there are more matches
}
} else if (node.nodeType === 1) { // Node.ELEMENT_NODE
for (var i = 0; i < node.childNodes.length; i++) {
iterate_node(node.childNodes[i]); // run recursive on DOM
}
}
})(content); // any dom node
There's also highlight.js, which might be exactly what you want.