As I reported at in this CKEditor this ticket, inline widgets in CKEditor (4.7.0) do not preserve trailing spaces, causing display issues.
Take the following simple widget:
CKEDITOR.plugins.add('spanwidget', {
requires: 'widget',
init: function (editor) {
editor.widgets.add('spanwidget', {
editables: {
content: {
selector: 'span'
}
},
upcast: function (element) {
return element.name == 'span';
}
});
}
});
When you load the following data <span>lorem </span>ipsum, you see this text in output: loremipsum (notice the missing space).
This can be seen in this JSFiddle.
How can I work around the problem (I do not control which data is loaded inside CKEditor)?
I found a workaround to force the last trailing space to be preserved. The idea is to replace the last space with a when upcasting the widget element, then remove it before downcasting it:
CKEDITOR.replace('ck', {
allowedContent: true,
extraPlugins: 'spanwidget'
});
CKEDITOR.plugins.add('spanwidget', {
requires: 'widget',
init: function (editor) {
editor.widgets.add('spanwidget', {
editables: {
content: { selector: 'span' }
},
upcast: function (element) {
// Matches?
if (element.name == 'span') {
// Ends with text?
var children = element.children,
childCount = children.length,
lastChild = childCount && children[childCount - 1];
if (lastChild instanceof CKEDITOR.htmlParser.text) {
// Replace the last space with a non breaking space
// (see https://github.com/ckeditor/ckeditor-dev/issues/605)
lastChild.value = lastChild.value.replace(/ $/, ' ');
}
// Match!
return true;
}
// No match
return false;
},
downcast: function (element) {
// Span with class "targetinfo"?
if (element.name == 'span') {
// Ends with text?
var children = element.children,
childCount = children.length,
lastChild = childCount && children[childCount - 1];
if (lastChild instanceof CKEDITOR.htmlParser.text) {
// Ends with a non breaking space?
var match = lastChild.value.match(/ $/i);
if (match) {
// Replace the non breaking space with a normal one
lastChild.value = lastChild.value.replace(/ $/i, ' ');
// Clone the element
var clone = element.clone();
// Reinsert all the children into that element
for (var i = 0; i < childCount; i++) {
clone.add(children[i]);
}
// Return the clone
return clone;
}
}
}
}
});
}
});
See updated JSFiddle here.
Related
I am trying to use mCustomScrollbar on jqgrid( 4.9.2). The design of the scrollbar is getting changed but when scrolling horizontally the top headers does not move as they do normally.
The example I am trying to work on is of collapsable grid.
and for the mCustomScroll
$(".ui-jqgrid-bdiv").mCustomScrollbar({
axis:"yx",
});
Is it not possible at all to use any custom scroll bar on jqgrid ?
I had made some customized changes to jqgr so migrating to the other version will be a tough task, so Instead, I made the changes in the mcustomscrollbar and posting the answer so if any one else come across the same problem it will be beneficial.
so there is a methods _tweetTo, which is called for the container on which the scrollbar is assigned by the initializing as
$(".ui-jqgrid-bdiv").mCustomScrollbar({
axis:"yx",
});
now just after the method call _tweenTo(line# 2131 for V:3.1.5) insert the following code
if ($(".ui-jqgrid-hdiv").length > 0) {
$(".ui-jqgrid-view").css("overflow", "hidden");
$(".ui-jqgrid-hdiv").css("width", $("#grid1").width() + "px"); // grid1 is the id of your gridcontainer/table
_tweenTo($(".ui-jqgrid-hdiv")[0], property, Math.round(scrollTo[0]), dur[0], options.scrollEasing, options.overwrite, {
onStart: function () {
if (options.callbacks && options.onStart && !d.tweenRunning) {
/* callbacks: onScrollStart */
if (_cb("onScrollStart")) { _mcs(); o.callbacks.onScrollStart.call(el[0]); }
d.tweenRunning = true;
_onDragClasses(mCSB_dragger);
d.cbOffsets = _cbOffsets();
}
}, onUpdate: function () {
if (options.callbacks && options.onUpdate) {
/* callbacks: whileScrolling */
if (_cb("whileScrolling")) { _mcs(); o.callbacks.whileScrolling.call(el[0]); }
}
}, onComplete: function () {
if (options.callbacks && options.onComplete) {
if (o.axis === "yx") { clearTimeout(mCSB_container[0].onCompleteTimeout); }
var t = mCSB_container[0].idleTimer || 0;
mCSB_container[0].onCompleteTimeout = setTimeout(function () {
/* callbacks: onScroll, onTotalScroll, onTotalScrollBack */
if (_cb("onScroll")) { _mcs(); o.callbacks.onScroll.call(el[0]); }
if (_cb("onTotalScroll") && scrollTo[1] >= limit[1] - totalScrollOffset && d.cbOffsets[0]) { _mcs(); o.callbacks.onTotalScroll.call(el[0]); }
if (_cb("onTotalScrollBack") && scrollTo[1] <= totalScrollBackOffset && d.cbOffsets[1]) { _mcs(); o.callbacks.onTotalScrollBack.call(el[0]); }
d.tweenRunning = false;
mCSB_container[0].idleTimer = 0;
_onDragClasses(mCSB_dragger, "hide");
}, t);
}
}
});
}
and in the method definition of _tweenTo there is another method _tween
update that method as
function _tween() {
// added condition so the top headers remains fixed
if (el.classList.contains("ui-jqgrid-hdiv") && prop == "top") {
return;
}
//ends here
if (duration > 0) {
tobj.currVal = _ease(tobj.time, from, diff, duration, easing);
elStyle[prop] = Math.round(tobj.currVal) + "px";
} else {
elStyle[prop] = to + "px";
}
onUpdate.call();
}
and the scrollbar is up and running.. !!
How can I do the following with the Ace Editor.
User types the '#' character
Autocomplete pops up
User makes a selection from the dropdown
The '#' gets removed now that the selection has been made
I basically want the # as a trigger for the autocomplete, but I don't want it hanging around after.
Thank you
addAutoComplete() {
var me = this;
ace.require('./config').loadModule('ace/ext/language_tools', () => {
this.aceEditor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: false
});
me.include = /[a-z.]/i;
this.aceEditor.on('change', (obj, editor) => {
switch (obj.action) {
case 'insert':
let lines = obj.lines;
let char = lines[0];
if ((lines.length === 1) && (char.length === 1) && me.include.test(char)) {
setTimeout(() => {
me.aceEditor.commands.byName.startAutocomplete.exec(me.aceEditor);
}, 50);
}
break;
}
});
});
}
that's the demo. works fine
We can bind the '#' keyword and trigger the autocomplete:
editor.commands.addCommand({
name: "myCommand",
bindKey: { win: "#", mac: "#" },
exec: function (editor) {
autocomplete();
}
});
autocomplete: function () {
staticWordCompleter = {
var getWordList = function(editor, session, pos, prefix, callback, isRHSEditor) {
var wordList = []; // add your words to this list
callback(null, wordList.map(function(word) {
return {
caption: word,
value: word
};
}));
editor.completers = [staticWordCompleter];
}
None of the above approaches worked for me. I ended up rolling my own autocomplete/autosuggest for this to work. It's hard to contrive an example but in short the steps are:
Capture the onChange of the editor
If the editor action meets the criteria (i.e. the # symbol is pressed), display a box at the cursor position. This can be done by setting the box to position absolute, and setting the top/left attributes:
const {row, column} = window.editor.getCursorPosition();
const {pageX, pageY} = window.editor.renderer.textToScreenCoordinates(row, column)
const autosuggest = document.getElementById("ELEMENT_NAME")
autosuggest.style.left = pageX
autosuggest.style.top = pageY
Add commands to disable/redirect any actions such as the arrow keys/enter key, and re-enable when a selection is made.
function disableEnterKeyInEditor() {
editor.commands.addCommand(commmand_enter);
}
function enableEnterKeyInEditor() {
editor.commands.removeCommands(commmand_enter);
}
command_enter = {
name: "enterKeyPressedCommand",
bindKey: {win: "Enter", mac: "Enter"},
exec: function (editor) {
return true;
}
};
This was much easier than working with ace's autocomplete.
You can check with below code:
var getWordList = function(editor, session, pos, prefix, callback, isRHSEditor) {
var wordList = [];
if(prefix === '#') {
wordList.push('add you're name list here');
}
callback(null, wordList.map(function(word) {
return {
caption: word,
value: word
};
}));
}
I found this working example of Inheritance Patterns that separates business logic and framework code. I'm tempted to use it as a boilerplate, but since it is an inheritance Pattern, then how can I extend the business logic (the methods in var Speaker)?
For instance, how can I extend a walk: method into it?
/**
* Object Speaker
* An object representing a person who speaks.
*/
var Speaker = {
init: function(options, elem) {
// Mix in the passed in options with the default options
this.options = $.extend({},this.options,options);
// Save the element reference, both as a jQuery
// reference and a normal reference
this.elem = elem;
this.$elem = $(elem);
// Build the dom initial structure
this._build();
// return this so we can chain/use the bridge with less code.
return this;
},
options: {
name: "No name"
},
_build: function(){
this.$elem.html('<h1>'+this.options.name+'</h1>');
},
speak: function(msg){
// You have direct access to the associated and cached jQuery element
this.$elem.append('<p>'+msg+'</p>');
}
};
// Make sure Object.create is available in the browser (for our prototypal inheritance)
// Courtesy of Papa Crockford
// Note this is not entirely equal to native Object.create, but compatible with our use-case
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() {} // optionally move this outside the declaration and into a closure if you need more speed.
F.prototype = o;
return new F();
};
}
$.plugin = function(name, object) {
$.fn[name] = function(options) {
// optionally, you could test if options was a string
// and use it to call a method name on the plugin instance.
return this.each(function() {
if ( ! $.data(this, name) ) {
$.data(this, name, Object.create(object).init(options, this));
}
});
};
};
// With the Speaker object, we could essentially do this:
$.plugin('speaker', Speaker);
Any ideas?
How about simply using JavaScript's regular prototype inheritance?
Consider this:
function Speaker(options, elem) {
this.elem = $(elem)[0];
this.options = $.extend(this.defaults, options);
this.build();
}
Speaker.prototype = {
defaults: {
name: "No name"
},
build: function () {
$('<h1>', {text: this.options.name}).appendTo(this.elem);
return this;
},
speak: function(message) {
$('<p>', {text: message}).appendTo(this.elem);
return this;
}
};
Now you can do:
var pp = new Speaker({name: "Porky Pig"}, $("<div>").appendTo("body"));
pp.speak("That's all folks!");
Speaker.prototype.walk = function (destination) {
$('<p>', {
text: this.options.name + " walks " + destination + ".",
css: { color: "red" }
}).appendTo(this.elem);
return this;
}
pp.walk("off the stage");
Runnable version:
function Speaker(options, elem) {
this.elem = $(elem)[0];
this.options = $.extend(this.defaults, options);
this.build();
}
Speaker.prototype = {
defaults: {
name: "No name"
},
build: function () {
$('<h1>', {text: this.options.name}).appendTo(this.elem);
return this;
},
speak: function(message) {
$('<p>', {text: message}).appendTo(this.elem);
return this;
}
};
var pp = new Speaker({name: "Porky Pig"}, $("<div>").appendTo("body"));
pp.speak("That's all folks!");
Speaker.prototype.walk = function (destination) {
$('<p>', {
text: this.options.name + " walks " + destination + ".",
css: { color: "red" }
}).appendTo(this.elem);
return this;
}
pp.walk("off the stage");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
Is there a way to query jsPlumb in order to retrieve all connections whose "source" and "target" properties have the same parent div? Currently, I am manually setting the "scope" property of each connection (based on the parent div's id), and this works. However, feels hacky. I feel as if there should be some way to query jsPlumb like
jsPlumb.select('#parentDiv').each(function(connection) {
/*do stuff here*/
});
At the time of connection creation itself you can check and store those connections separately instead of checking for it later. Code:
jsPlumb.bind("jsPlumbConnection", function(ci) {
var s=ci.sourceId,c=ci.targetId;
var f=$('#'+s).parent().attr("id");
var g=$('#'+c).parent().attr("id");
if( f===g ){
// store ci.connection
console.log("Connection:"+ ci.connection +" has same parent div");
}
});
This is how you can confined it to a specific parent div & use 'instance' for each jsPlumb operation :
jsPlumb.ready(function () {
instance = jsPlumb.getInstance({
DragOptions: { cursor: 'pointer', zIndex: 2000 },
ConnectionOverlays: [
["Arrow", { width: 12, length: 15, location: -3 }],
],
Container: "parent" //put parent div id here
});
});
function containsCycle() {
var return_content = false;
$('.item:visible').each(function() {
$(this).addClass("white");
});
$('.item:visible').each(function() {
$vertex = $(this);
if ($vertex.hasClass("white")) {
if (visit($vertex)) {
return_content = true;
return false;
}
}
});
return return_content;
}
function visit(vertex) {
vertex.removeClass("white")
.addClass("grey");
var vertex_connections = jsPlumb
.getConnections({
source: vertex
});
for (var i = 0; i < vertex_connections.length; i++) {
if ($('#' + vertex_connections[i].targetId)
.hasClass("grey")) {
return true;
} else if ($('#' + vertex_connections[i].targetId)
.hasClass("white")) {
if (visit($('#' + vertex_connections[i].targetId))) {
return true;
}
}
}
vertex.removeClass("grey")
.addClass("black");
return false;
}
Use this code to find the cycle connection
I been trying to find out how to make a textarea field in my Spring Webflow project we are using Dojo (dijit) for the forms. can someone please help. below is my code!
<td valign="top"><form:input path="bio" class="value" /> <script
type="text/javascript">
Spring.addDecoration(new Spring.ElementDecoration({
elementId : "name",
widgetType : "dijit.form.ValidationTextBox",
widgetAttrs : {
promptMessage : "Please enter your name from 2 to 10 characters",
invalidMessage : "A 2 to 10 characters value is required.",
required : true,
regExp : "^[a-zA-Z]{2,10}$"
}
}));
</script> <br />
<p></td>
this should work:
<form:textarea id="bio" path="bio" />
<script type="text/javascript">
Spring.addDecoration(new Spring.ElementDecoration({
elementId : "bio",
widgetType : "dijit.form.SimpleTextarea",
</script>
[EDIT] I just checked and textArea's don't seem to have any kind of validation, so I created one for you based on ValidationTextBox.
just put this somewhere in your javascript file:
dojo.provide("dijit.form.ValidationTextArea");
dojo.require("dojo.i18n");
dojo.require("dijit.form.TextBox");
dojo.require("dijit.Tooltip");
dojo.requireLocalization("dijit.form", "validate");
/*=====
dijit.form.ValidationTextBox.__Constraints = function(){
// locale: String
// locale used for validation, picks up value from this widget's lang attribute
// _flags_: anything
// various flags passed to regExpGen function
this.locale = "";
this._flags_ = "";
}
=====*/
dojo.declare(
"dijit.form.ValidationTextArea",
dijit.form.TextBox,
{
// summary:
// Base class for textbox widgets with the ability to validate content of various types and provide user feedback.
// tags:
// protected
templateString: "<textarea name=${name} ${nameAttrSetting} dojoAttachPoint='focusNode,containerNode,textbox' autocomplete='off'></textarea>",
baseClass: "dijitTextArea",
attributeMap: dojo.delegate(dijit.form._FormValueWidget.prototype.attributeMap, {
rows:"textbox", cols: "textbox"
}),
// rows: Number
// The number of rows of text.
rows: "3",
// rows: Number
// The number of characters per line.
cols: "20",
// required: Boolean
// User is required to enter data into this field.
required: false,
// promptMessage: String
// If defined, display this hint string immediately on focus to the textbox, if empty.
// Think of this like a tooltip that tells the user what to do, not an error message
// that tells the user what they've done wrong.
//
// Message disappears when user starts typing.
promptMessage: "",
// invalidMessage: String
// The message to display if value is invalid.
invalidMessage: "$_unset_$", // read from the message file if not overridden
// constraints: dijit.form.ValidationTextBox.__Constraints
// user-defined object needed to pass parameters to the validator functions
constraints: {},
// regExp: [extension protected] String
// regular expression string used to validate the input
// Do not specify both regExp and regExpGen
regExp: "(.|[\r\n])*",
regExpGen: function(/*dijit.form.ValidationTextBox.__Constraints*/constraints){
// summary:
// Overridable function used to generate regExp when dependent on constraints.
// Do not specify both regExp and regExpGen.
// tags:
// extension protected
return this.regExp; // String
},
// state: [readonly] String
// Shows current state (ie, validation result) of input (Normal, Warning, or Error)
state: "",
// tooltipPosition: String[]
// See description of `dijit.Tooltip.defaultPosition` for details on this parameter.
tooltipPosition: [],
_setValueAttr: function(){
// summary:
// Hook so attr('value', ...) works.
this.inherited(arguments);
this.validate(this._focused);
},
validator: function(/*anything*/value, /*dijit.form.ValidationTextBox.__Constraints*/constraints){
// summary:
// Overridable function used to validate the text input against the regular expression.
// tags:
// protected
return (new RegExp("^(?:" + this.regExpGen(constraints) + ")"+(this.required?"":"?")+"$")).test(value) &&
(!this.required || !this._isEmpty(value)) &&
(this._isEmpty(value) || this.parse(value, constraints) !== undefined); // Boolean
},
_isValidSubset: function(){
// summary:
// Returns true if the value is either already valid or could be made valid by appending characters.
// This is used for validation while the user [may be] still typing.
return this.textbox.value.search(this._partialre) == 0;
},
isValid: function(/*Boolean*/ isFocused){
// summary:
// Tests if value is valid.
// Can override with your own routine in a subclass.
// tags:
// protected
return this.validator(this.textbox.value, this.constraints);
},
_isEmpty: function(value){
// summary:
// Checks for whitespace
return /^\s*$/.test(value); // Boolean
},
getErrorMessage: function(/*Boolean*/ isFocused){
// summary:
// Return an error message to show if appropriate
// tags:
// protected
return this.invalidMessage; // String
},
getPromptMessage: function(/*Boolean*/ isFocused){
// summary:
// Return a hint message to show when widget is first focused
// tags:
// protected
return this.promptMessage; // String
},
_maskValidSubsetError: true,
validate: function(/*Boolean*/ isFocused){
// summary:
// Called by oninit, onblur, and onkeypress.
// description:
// Show missing or invalid messages if appropriate, and highlight textbox field.
// tags:
// protected
var message = "";
var isValid = this.disabled || this.isValid(isFocused);
if(isValid){ this._maskValidSubsetError = true; }
var isValidSubset = !isValid && isFocused && this._isValidSubset();
var isEmpty = this._isEmpty(this.textbox.value);
if(isEmpty){ this._maskValidSubsetError = true; }
this.state = (isValid || (!this._hasBeenBlurred && isEmpty) || isValidSubset) ? "" : "Error";
if(this.state == "Error"){ this._maskValidSubsetError = false; }
this._setStateClass();
dijit.setWaiState(this.focusNode, "invalid", isValid ? "false" : "true");
if(isFocused){
if(isEmpty){
message = this.getPromptMessage(true);
}
if(!message && (this.state == "Error" || (isValidSubset && !this._maskValidSubsetError))){
message = this.getErrorMessage(true);
}
}
this.displayMessage(message);
return isValid;
},
// _message: String
// Currently displayed message
_message: "",
displayMessage: function(/*String*/ message){
// summary:
// Overridable method to display validation errors/hints.
// By default uses a tooltip.
// tags:
// extension
if(this._message == message){ return; }
this._message = message;
dijit.hideTooltip(this.domNode);
if(message){
dijit.showTooltip(message, this.domNode, this.tooltipPosition);
}
},
_refreshState: function(){
// Overrides TextBox._refreshState()
this.validate(this._focused);
this.inherited(arguments);
},
//////////// INITIALIZATION METHODS ///////////////////////////////////////
constructor: function(){
this.constraints = {};
},
postMixInProperties: function(){
// Copy value from srcNodeRef, unless user specified a value explicitly (or there is no srcNodeRef)
if(!this.value && this.srcNodeRef){
this.value = this.srcNodeRef.value;
}
this.inherited(arguments);
this.constraints.locale = this.lang;
this.messages = dojo.i18n.getLocalization("dijit.form", "validate", this.lang);
if(this.invalidMessage == "$_unset_$"){ this.invalidMessage = this.messages.invalidMessage; }
var p = this.regExpGen(this.constraints);
this.regExp = p;
var partialre = "";
// parse the regexp and produce a new regexp that matches valid subsets
// if the regexp is .* then there's no use in matching subsets since everything is valid
if(p != ".*"){ this.regExp.replace(/\\.|\[\]|\[.*?[^\\]{1}\]|\{.*?\}|\(\?[=:!]|./g,
function (re){
switch(re.charAt(0)){
case '{':
case '+':
case '?':
case '*':
case '^':
case '$':
case '|':
case '(':
partialre += re;
break;
case ")":
partialre += "|$)";
break;
default:
partialre += "(?:"+re+"|$)";
break;
}
}
);}
try{ // this is needed for now since the above regexp parsing needs more test verification
"".search(partialre);
}catch(e){ // should never be here unless the original RE is bad or the parsing is bad
partialre = this.regExp;
console.warn('RegExp error in ' + this.declaredClass + ': ' + this.regExp);
} // should never be here unless the original RE is bad or the parsing is bad
this._partialre = "^(?:" + partialre + ")$";
},
filter: function(/*String*/ value){
// Override TextBox.filter to deal with newlines... specifically (IIRC) this is for IE which writes newlines
// as \r\n instead of just \n
if(value){
value = value.replace(/\r/g,"");
}
return this.inherited(arguments);
},
_setDisabledAttr: function(/*Boolean*/ value){
this.inherited(arguments); // call FormValueWidget._setDisabledAttr()
this._refreshState();
},
_setRequiredAttr: function(/*Boolean*/ value){
this.required = value;
dijit.setWaiState(this.focusNode,"required", value);
this._refreshState();
},
postCreate: function(){
if(dojo.isIE){ // IE INPUT tag fontFamily has to be set directly using STYLE
var s = dojo.getComputedStyle(this.focusNode);
if(s){
var ff = s.fontFamily;
if(ff){
this.focusNode.style.fontFamily = ff;
}
}
}
this.inherited(arguments);
if(dojo.isIE && this.cols){ // attribute selectors is not supported in IE6
dojo.addClass(this.textbox, "dijitTextAreaCols");
}
},
reset:function(){
// Overrides dijit.form.TextBox.reset() by also
// hiding errors about partial matches
this._maskValidSubsetError = true;
this.inherited(arguments);
},
_onBlur: function(){
this.displayMessage('');
this.inherited(arguments);
},
_previousValue: "",
_onInput: function(/*Event?*/ e){
// Override TextBox._onInput() to enforce maxLength restriction
if(this.maxLength){
var maxLength = parseInt(this.maxLength);
var value = this.textbox.value.replace(/\r/g,'');
var overflow = value.length - maxLength;
if(overflow > 0){
if(e){ dojo.stopEvent(e); }
var textarea = this.textbox;
if(textarea.selectionStart){
var pos = textarea.selectionStart;
var cr = 0;
if(dojo.isOpera){
cr = (this.textbox.value.substring(0,pos).match(/\r/g) || []).length;
}
this.textbox.value = value.substring(0,pos-overflow-cr)+value.substring(pos-cr);
textarea.setSelectionRange(pos-overflow, pos-overflow);
}else if(dojo.doc.selection){ //IE
textarea.focus();
var range = dojo.doc.selection.createRange();
// delete overflow characters
range.moveStart("character", -overflow);
range.text = '';
// show cursor
range.select();
}
}
this._previousValue = this.textbox.value;
}
this.inherited(arguments);
}
}
);
you can use it the way you use ValidationTextBox, just use ValidationTextArea instead.