I'd like to implement transient highlighting in a Quill document.
For example, imagine a SEARCH button where the user can highlight all instances of a keyword in the current document, by setting the text color for matching text ranges.
I can do that today, with something like this:
var keyword = "hello";
var text = quill.getText();
var matchIndex = text.indexOf(keyword);
while (matchIndex >= 0) {
quill.formatText(matchIndex, keyword.length, { "color" : "#f00" });
matchIndex = text.indexOf(keyword, matchIndex + keyword.length);
}
But I don't want the resultant deltas to be incorporated into the official change history of this document. These are just transient highlights, and I'd like to be able to clear them all away with something like this...
quill.clearTransientFormats();
I'd even like to give the user the choice of leaving the transient highlights enabled, while they continue to edit and modify the document with their own destructive changes.
Essentially, I want to have two different kinds of formatting:
Destructive formatting is always recorded in the sequence of deltas persisted in the document history.
Transient formatting is ignored in the document history, since it only applies to the current view.
What's the best way to implement something like this?
I would recommend just post-processing the delta before saving. It can be achieved fairly easily with compose:
var length = aboutToBeStored.length();
var toStore = aboutToBeStored.compose(new Delta().retain(0, length, { color: null }));
Related
I have this code:
//Callback function
var done = arguments[arguments.length - 1];
//Take all the events
var array_events = []
var retour = (e) => {
array_events.push(e.target.outerHTML)
}
var quit = (key) => {
console.log(array_events);
(key.keyCode == 27 )? done(JSON.stringify(array_events)) : undefined
}
// Listen to the clicks
getPath = document.addEventListener("click", retour, true)
getIndex = document.addEventListener("click", detour, true)
// Listen to the key "esc" which means user has gathered all needed events
getKey = document.addEventListener("keydown", quit, true)
This gets me the value of outerHTML of an element clicked, so far so good.
I'm building a program that needs to find that element,but in the case where the outerHTML of the element is not unique (meaning there are multiple occurrences of that element in the DOM), I cannot find out which one of the elements the user wants, so I think the easiest way would be to include index of some sort.
My question is ->
How can I get the index of that element relative to whole HTML document?
If that is not possible, any index of some sort that I can take and calculate which one is first or second or last does help too!
I am not aware of any indexing of DOM elements that is accessible with JS.
Not sure how you want to use this information about what element has been selected. So here are several ideas, hope one will be useful.
If you need to do something with that element, like modify it in some way in your callback, then you may want to use:
e.target.parentElement
This will return the actual DOM element, and you can do whatever you want with it. It does not matter how many copies you have in the page. And if you actually need the HTML text, you can always use .innerHTML to get it.
If you need this for some type of analytics where you want to save this interaction somewhere instead of modifying the page you can do something like this:
e.target.parentElement.getBoundingClientRect()
This will return an object that looks something like this:
this way you will know the exact coordinates of the element that was selected, and even if you have multiple elements with the exact same HTML you can know that this is some specific copy of it.
If you do not have too many elements that need to be affected by this, maybe you can add the index yourself. When all elements are created, you can do something like this:
let maxId = 0;
const allElements = document.querySelectorAll('.SelectorForYourElement');
allElements.forEach(element => {
element.dataset.id = ++maxId;
});
How can I make each of this element separated. Currently, each elements is merged into one <span> element
<span style="font-family:wingdings;">.4=??????</span>
<span style="font-family:symbol;">QPGH</span>
this is generated by code below
import Command from '#ckeditor/ckeditor5-core/src/command';
export default class SymbolsCommand extends Command {
execute({ charButtons }) {
const { model } = this.editor;
model.change((writer) => {
const { selection } = model.document;
const position = writer.createPositionAt(selection.getFirstPosition());
console.log(charButtons, 'charButtons');
const renderChars = () => charButtons.map(({ fontFamily, char }) => {
writer.insertText(char, { fontFamily }, position);
});
return renderChars();
});
}
refresh() {
this.isEnabled = true;
}
}
I expect the output like
<span style="font-family:wingdings;">=</span>
<span style="font-family:wingdings;">?</span>
<span style="font-family:wingdings;">4</span>
<span style="font-family:symbol;">4</span>
<span style="font-family:symbol;">4</span>
...
Joining similar <span>s is a default CKEditor 5 behavior. One of the reasons is that those characters are also joined in the data model and represented by a one text node. It doesn't matter if you insert those characters one by one or all at once, if they have the same attributes they are grouped together.
One of the ways to prevent that from happening is to specify view.AttributeElement#id. This is unfortunately a more advanced subject. Among other, you will have to provide converters that will create attribute elements in the view.
I think there are two ways to achieve your goal, both will require you to add a converter instead of relying on fontFamily converter from font family plugin (I assume this happens here).
Using attribute on text with unique AttributeElement#id to prevent joining <span>s
The first solution is to introduce a new attribute for text (remember about extending schema) and provide converter for it. Let's call the attribute key symbol.
The converter would have to convert given text node character-by-character and set unique id for each created attribute span.
This is a more complicated solution, although probably a better one.
editor.model.conversion.for( 'downcast' ).add( dispatcher => {
dispatcher.on( 'attribute:symbol', ( evt, data, conversionApi ) => {
// Provide your converter here. It should take `data.item`, iterate
// through it's text content (`data.item.data`), use
// `conversionApi.writer` to create attribute elements with unique ids
// and use the writer and `conversionApi.mapper` to place them in the view.
} );
} );
You could base the converter on function wrap from downcasthelpers.js in the engine: https://github.com/ckeditor/ckeditor5-engine/blob/master/src/conversion/downcasthelpers.js.
Insert symbols as inline elements
Another solution would be to insert elements with characters instead of simply text nodes.
In this case, again, you have to specify the new model element in the schema. Maybe extending the '$text' item.
For conversion, you could probably use elementToElement helpers from editor.conversion.for(). For downcast you would have to specify view as a callback and set unique id there (unique ids could be simply a counter, incremented by one each time). If elementToElement won't work for downcasting (it should work for upcast) you will need to provide a custom converter through .for( 'downcast' ).add( ... ).
This solution is easier but I am not sure it will work. It's hard to say which one is better because it depends also on what exactly you want to achieve. I'd probably try both but I'd focus on trying to do it using the first approach.
I wish there was an easier way to achieve this at the moment but this use case is quite rare, so the architecture was focused in another direction.
I've created a very basic script that allows the user to replace a specific font in all the document (even LayerSets) by another one.
A practice example: I want to substitute all Arial-Bold by Arial-Italic, but some of the TextLayers have Arial-Bold and Arial-Regular inside the same Layer, how can make that the script only changes the Arial-Bold part of the TextLayer and not the whole layer?
Code I'm currently using:
var inFont = prompt("write inFont","Write inFont");
var outFont = prompt("write outFont","Write outFont");
app.preferences.typeUnits = TypeUnits.PIXELS;
var doc = app.activeDocument;
function changeFonts(target){
var layers = target.layers;
for(var i=0;i<layers.length;i++){
if(layers[i].typename == "LayerSet"){
changeFonts(layers[i]);
} else {
if((layers[i].kind == LayerKind.TEXT) && (layers[i].textItem.font == inFont)) {
layers[i].textItem.font = outFont;
};
};
};
};
changeFonts(doc);
I would assume you would have to crawl through the text and look at each individual character. Here is an article that that talks about formatting specific character ranges. Formatting text ranges.
You could use something like this to loop through the text one character at a time, check the formatting and change it if needed. I can't think of any other way to approach this.
I'm trying to access the live content from each instance of CKEditor so I can setup a total word count. Before using CKEditor I would get the textarea's content with .getElementById(), and then I would get the live word count by passing the textarea element into my Countable() function which appends an event listener to the area. Is there a way to grab the live content of a CKEditor instance? I know it's an iframe so I'm not sure if it's possible to grab the live content.
Code I used to use with simple textarea:
var area1 = document.getElementById('textarea1');
Countable.live(area1, function(counter1) {
jQuery("#word_count1").text(counter1.words);
a1_count = counter1.words;
total_count();
});
This depends a lot on your Countable function and it's requirements - it would have helped to seen it and to know it's requirements. You can get the contents of each CKEditor instance in a few different methods, this is one
var contentArray = [];
var i = 0;
for (var instance in CKEDITOR.instances) {
var tmpEditor = CKEDITOR.instances[instance];
contentArray[i] = tmpEditor.getData();
i++;
}
Now the contents are in the contentArray. But form your code it looks like Countable needs an element. I'm unsure as to what kind of element reference it can use, but something like this might get you further:
var editor = CKEDITOR.instances.editor1; // change "editor1" to suit your editor
var element = editor.editable().$; // Get a reference to the body of the instance
Countable.live(element, function(counter1) {
jQuery("#word_count1").text(counter1.words);
a1_count = counter1.words;
total_count();
});
Now of course this only supports one editor instance, but the two examples combined might do the trick. Hopefully you don't use inline instances (it needs some additional work but is doable). Also note that naturally these return the source, not the text.
Also not that you do not want to loop this very quickly, it is very cpu intensive. I recommend a slow loop combined with the the CKEditor change event and maybe some other events that trigger the update. Do not trigger on every change, rather set a timeout to buffer the update trigger (you don't want to do the update when the user is typing).
I'm making a highlighting plugin for a client to find things in a page and I decided to test it with a help viewer im still building but I'm having an issue that'll (probably) require some regex.
I do not want to parse HTML, and im totally open on how to do this differently, this just seems like the the best/right way.
http://oscargodson.com/labs/help-viewer
http://oscargodson.com/labs/help-viewer/js/jquery.jhighlight.js
Type something in the search... ok, refresh the page, now type, like, class or class=" or type <a you'll notice it'll search the actual HTML (as expected). How can I only search the text?
If i do .text() it'll vaporize all the HTML and what i get back will just be a big blob of text, but i still want the HTML so I dont lose formatting, links, images, etc. I want this to work like CMD/CTRL+F.
You'd use this plugin like:
$('article').jhighlight({find:'class'});
To remove them:
.jhighlight('remove')
==UPDATE==
While Mike Samuel's idea below does in fact work, it's a tad heavy for this plugin. It's mainly for a client looking to erase bad words and/or MS Word characters during a "publishing" process of a form. I'm looking for a more lightweight fix, any ideas?
You really don't want to use eval, mess with innerHTML or parse the markup "manually". The best way, in my opinion, is to deal with text nodes directly and keep a cache of the original html to erase the highlights. Quick rewrite, with comments:
(function($){
$.fn.jhighlight = function(opt) {
var options = $.extend($.fn.jhighlight.defaults, opt)
, txtProp = this[0].textContent ? 'textContent' : 'innerText';
if ($.trim(options.find.length) < 1) return this;
return this.each(function(){
var self = $(this);
// use a cache to clear the highlights
if (!self.data('htmlCache'))
self.data('htmlCache', self.html());
if(opt === 'remove'){
return self.html( self.data('htmlCache') );
}
// create Tree Walker
// https://developer.mozilla.org/en/DOM/treeWalker
var walker = document.createTreeWalker(
this, // walk only on target element
NodeFilter.SHOW_TEXT,
null,
false
);
var node
, matches
, flags = 'g' + (!options.caseSensitive ? 'i' : '')
, exp = new RegExp('('+options.find+')', flags) // capturing
, expSplit = new RegExp(options.find, flags) // no capturing
, highlights = [];
// walk this wayy
// and save matched nodes for later
while(node = walker.nextNode()){
if (matches = node.nodeValue.match(exp)){
highlights.push([node, matches]);
}
}
// must replace stuff after the walker is finished
// otherwise replacing a node will halt the walker
for(var nn=0,hln=highlights.length; nn<hln; nn++){
var node = highlights[nn][0]
, matches = highlights[nn][1]
, parts = node.nodeValue.split(expSplit) // split on matches
, frag = document.createDocumentFragment(); // temporary holder
// add text + highlighted parts in between
// like a .join() but with elements :)
for(var i=0,ln=parts.length; i<ln; i++){
// non-highlighted text
if (parts[i].length)
frag.appendChild(document.createTextNode(parts[i]));
// highlighted text
// skip last iteration
if (i < ln-1){
var h = document.createElement('span');
h.className = options.className;
h[txtProp] = matches[i];
frag.appendChild(h);
}
}
// replace the original text node
node.parentNode.replaceChild(frag, node);
};
});
};
$.fn.jhighlight.defaults = {
find:'',
className:'jhighlight',
color:'#FFF77B',
caseSensitive:false,
wrappingTag:'span'
};
})(jQuery);
If you're doing any manipulation on the page, you might want to replace the caching with another clean-up mechanism, not trivial though.
You can see the code working here: http://jsbin.com/anace5/2/
You also need to add display:block to your new html elements, the layout is broken on a few browsers.
In the javascript code prettifier, I had this problem. I wanted to search the text but preserve tags.
What I did was start with HTML, and decompose that into two bits.
The text content
Pairs of (index into text content where a tag occurs, the tag content)
So given
Lorem <b>ipsum</b>
I end up with
text = 'Lorem ipsum'
tags = [6, '<b>', 10, '</b>']
which allows me to search on the text, and then based on the result start and end indices, produce HTML including only the tags (and only balanced tags) in that range.
Have a look here: getElementsByTagName() equivalent for textNodes.
You can probably adapt one of the proposed solutions to your needs (i.e. iterate over all text nodes, replacing the words as you go - this won't work in cases such as <tag>wo</tag>rd but it's better than nothing, I guess).
I believe you could just do:
$('#article :not(:has(*))').jhighlight({find : 'class'});
Since it grabs all leaf nodes in the article it would require valid xhtml, that is, it would only match link in the following example:
<p>This is some paragraph content with a link</p>
DOM traversal / selector application could slow things down a bit so it might be good to do:
article_nodes = article_nodes || $('#article :not(:has(*))');
article_nodes.jhighlight({find : 'class'});
May be something like that could be helpful
>+[^<]*?(s(<[\s\S]*?>)?e(<[\s\S]*?>)?e)[^>]*?<+
The first part >+[^<]*? finds > of the last preceding tag
The third part [^>]*?<+ finds < of the first subsequent tag
In the middle we have (<[\s\S]*?>)? between characters of our search phrase (in this case - "see").
After regular expression searching you could use the result of the middle part to highlight search phrase for user.