diff --git a/Gruntfile.js b/Gruntfile.js index 95c8d6989..0825e1b39 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,5 @@ /** - * Copyright 2013, 2015 IBM Corp. + * Copyright 2013, 2016 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,6 +104,8 @@ module.exports = function(grunt) { "editor/js/settings.js", "editor/js/user.js", "editor/js/comms.js", + "editor/js/bidi.js", + "editor/js/format.js", "editor/js/ui/state.js", "editor/js/nodes.js", "editor/js/history.js", diff --git a/editor/js/bidi.js b/editor/js/bidi.js new file mode 100644 index 000000000..11e2ec84a --- /dev/null +++ b/editor/js/bidi.js @@ -0,0 +1,141 @@ +/** + * Copyright 2016 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +RED.bidi = (function() { + var textDir = ""; + var LRE = "\u202A", + RLE = "\u202B", + PDF = "\u202C"; + + function isRTLValue(stringValue) { + for (var ch in stringValue) { + if (isBidiChar(stringValue.charCodeAt(ch))) { + return true; + } + else if(isLatinChar(stringValue.charCodeAt(ch))) { + return false; + } + } + return false; + } + + function isBidiChar(c) { + if (c >= 0x05d0 && c <= 0x05ff) { + return true; + } + else if (c >= 0x0600 && c <= 0x065f) { + return true; + } + else if (c >= 0x066a && c <= 0x06ef) { + return true; + } + else if (c >= 0x06fa && c <= 0x07ff) { + return true; + } + else if (c >= 0xfb1d && c <= 0xfdff) { + return true; + } + else if (c >= 0xfe70 && c <= 0xfefc) { + return true; + } + else { + return false; + } + } + + function isLatinChar(c){ + if((c > 64 && c < 91)||(c > 96 && c < 123)) { + return true; + } + else { + return false; + } + } + + /** + * Determines the text direction of a given string. + * @param value - the string + */ + function resolveBaseTextDir(value) { + if (textDir == "auto") { + if (isRTLValue(value)) { + return "rtl"; + } else { + return "ltr"; + } + } + else { + return textDir; + } + } + + function onInputChange() { + $(this).attr("dir", resolveBaseTextDir($(this).val())); + } + + /** + * Listens to keyup, paste and cut events of a given input field. Upon one of these events the text direction is computed again + * @param input - the input field + */ + function initInputEvents(input) { + input.on("keyup",onInputChange).on("paste",onInputChange).on("cut",onInputChange); + } + + /** + * Enforces the text direction of a given string by adding UCC (Unicode Control Characters) + * @param value - the string + */ + function enforceTextDirectionWithUCC(value) { + if (value) { + var dir = resolveBaseTextDir(value); + if (dir == "ltr") { + return LRE + value + PDF; + } + else if (dir == "rtl") { + return RLE + value + PDF; + } + } + return value; + } + + /** + * Enforces the text direction for all the spans with style bidiAware under workpsace or sidebar div + */ + function enforceTextDirectionOnPage() { + $("#workspace").find('span.bidiAware').each(function() { + $(this).attr("dir", resolveBaseTextDir($(this).html())); + }); + $("#sidebar").find('span.bidiAware').each(function() { + $(this).attr("dir", resolveBaseTextDir($(this).text())); + }); + } + + /** + * Sets the text direction preference + * @param dir - the text direction preference + */ + function setTextDirection(dir) { + textDir = dir; + } + + return { + setTextDirection: setTextDirection, + enforceTextDirectionOnPage: enforceTextDirectionOnPage, + enforceTextDirectionWithUCC: enforceTextDirectionWithUCC, + resolveBaseTextDir: resolveBaseTextDir, + initInputEvents: initInputEvents + } +})(); diff --git a/editor/js/format.js b/editor/js/format.js new file mode 100644 index 000000000..8e3dd52e6 --- /dev/null +++ b/editor/js/format.js @@ -0,0 +1,1329 @@ +/** + * Copyright 2016 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +RED.format = (function() { + + var TextSegment = (function() { + var TextSegment = function (obj) { + this.content = ""; + this.actual = ""; + this.textDirection = ""; + this.localGui = ""; + this.isVisible = true; + this.isSeparator = false; + this.isParsed = false; + this.keep = false; + this.inBounds = false; + this.inPoints = false; + var prop = ""; + for (prop in obj) { + if (obj.hasOwnProperty(prop)) { + this[prop] = obj[prop]; + } + } + }; + return TextSegment; + })(); + + var tools = (function() { + function initBounds(bounds) { + if (!bounds) { + return false; + } + if (typeof(bounds.start) === "undefined") { + bounds.start = ""; + } + if (typeof(bounds.end) === "undefined") { + bounds.end = ""; + } + if (typeof(bounds.startAfter) !== "undefined") { + bounds.start = bounds.startAfter; + bounds.after = true; + } else { + bounds.after = false; + } + if (typeof(bounds.endBefore) !== "undefined") { + bounds.end = bounds.endBefore; + bounds.before = true; + } else { + bounds.before = false; + } + var startPos = parseInt(bounds.startPos, 10); + if (!isNaN(startPos)) { + bounds.usePos = true; + } else { + bounds.usePos = false; + } + var bLength = parseInt(bounds.length, 10); + if (!isNaN(bLength)) { + bounds.useLength = true; + } else { + bounds.useLength = false; + } + bounds.loops = typeof(bounds.loops) !== "undefined" ? !!bounds.loops : true; + return true; + } + + function getBounds(segment, src) { + var bounds = {}; + for (var prop in src) { + if (src.hasOwnProperty(prop)) { + bounds[prop] = src[prop]; + } + } + var content = segment.content; + var usePos = bounds.usePos && bounds.startPos < content.length; + if (usePos) { + bounds.start = ""; + bounds.loops = false; + } + bounds.bStart = usePos ? bounds.startPos : bounds.start.length > 0 ? content.indexOf(bounds.start) : 0; + var useLength = bounds.useLength && bounds.length > 0 && bounds.bStart + bounds.length < content.length; + if (useLength) { + bounds.end = ""; + } + bounds.bEnd = useLength ? bounds.bStart + bounds.length : bounds.end.length > 0 ? + content.indexOf(bounds.end, bounds.bStart + bounds.start.length) + 1 : content.length; + if (!bounds.after) { + bounds.start = ""; + } + if (!bounds.before) { + bounds.end = ""; + } + return bounds; + } + + return { + handleSubcontents: function (segments, args, subs, origContent, locale) { // jshint unused: false + if (!subs.content || typeof(subs.content) !== "string" || subs.content.length === 0) { + return segments; + } + var sLoops = true; + if (typeof(subs.loops) !== "undefined") { + sLoops = !!subs.loops; + } + for (var j = 0; true; j++) { + if (j >= segments.length) { + break; + } + if (segments[j].isParsed || segments.keep || segments[j].isSeparator) { + continue; + } + var content = segments[j].content; + var start = content.indexOf(subs.content); + if (start < 0) { + continue; + } + var end; + var length = 0; + if (subs.continued) { + do { + length++; + end = content.indexOf(subs.content, start + length * subs.content.length); + } while (end === 0); + } else { + length = 1; + } + end = start + length * subs.content.length; + segments.splice(j, 1); + if (start > 0) { + segments.splice(j, 0, new TextSegment({ + content: content.substring(0, start), + localGui: args.dir, + keep: true + })); + j++; + } + segments.splice(j, 0, new TextSegment({ + content: content.substring(start, end), + textDirection: subs.subDir, + localGui: args.dir + })); + if (end < content.length) { + segments.splice(j + 1, 0, new TextSegment({ + content: content.substring(end, content.length), + localGui: args.dir, + keep: true + })); + } + if (!sLoops) { + break; + } + } + }, + + handleBounds: function (segments, args, aBounds, origContent, locale) { + for (var i = 0; i < aBounds.length; i++) { + if (!initBounds(aBounds[i])) { + continue; + } + for (var j = 0; true; j++) { + if (j >= segments.length) { + break; + } + if (segments[j].isParsed || segments[j].inBounds || segments.keep || segments[j].isSeparator) { + continue; + } + var bounds = getBounds(segments[j], aBounds[i]); + var start = bounds.bStart; + var end = bounds.bEnd; + if (start < 0 || end < 0) { + continue; + } + var content = segments[j].content; + + segments.splice(j, 1); + if (start > 0) { + segments.splice(j, 0, new TextSegment({ + content: content.substring(0, start), + localGui: args.dir, + keep: true + })); + j++; + } + if (bounds.start) { + segments.splice(j, 0, new TextSegment({ + content: bounds.start, + localGui: args.dir, + isSeparator: true + })); + j++; + } + segments.splice(j, 0, new TextSegment({ + content: content.substring(start + bounds.start.length, end - bounds.end.length), + textDirection: bounds.subDir, + localGui: args.dir, + inBounds: true + })); + if (bounds.end) { + j++; + segments.splice(j, 0, new TextSegment({ + content: bounds.end, + localGui: args.dir, + isSeparator: true + })); + } + if (end + bounds.end.length < content.length) { + segments.splice(j + 1, 0, new TextSegment({ + content: content.substring(end + bounds.end.length, content.length), + localGui: args.dir, + keep: true + })); + } + if (!bounds.loops) { + break; + } + } + } + for (i = 0; i < segments.length; i++) { + segments[i].inBounds = false; + } + return segments; + }, + + handleCases: function (segments, args, cases, origContent, locale) { + if (cases.length === 0) { + return segments; + } + var hArgs = {}; + for (var prop in args) { + if (args.hasOwnProperty(prop)) { + hArgs[prop] = args[prop]; + } + } + for (var i = 0; i < cases.length; i++) { + if (!cases[i].handler || typeof(cases[i].handler.handle) !== "function") { + cases[i].handler = args.commonHandler; + } + if (cases[i].args) { + hArgs.cases = cases[i].args.cases; + hArgs.points = cases[i].args.points; + hArgs.bounds = cases[i].args.bounds; + hArgs.subs = cases[i].args.subs; + } else { + hArgs.cases = []; + hArgs.points = []; + hArgs.bounds = []; + hArgs.subs = {}; + } + cases[i].handler.handle(origContent, segments, hArgs, locale); + } + return segments; + }, + + handlePoints: function (segments, args, points, origContent, locale) { //jshint unused: false + for (var i = 0; i < points.length; i++) { + for (var j = 0; true; j++) { + if (j >= segments.length) { + break; + } + if (segments[j].isParsed || segments[j].keep || segments[j].isSeparator) { + continue; + } + var content = segments[j].content; + var pos = content.indexOf(points[i]); + if (pos >= 0) { + segments.splice(j, 1); + if (pos > 0) { + segments.splice(j, 0, new TextSegment({ + content: content.substring(0, pos), + textDirection: args.subDir, + localGui: args.dir, + inPoints: true + })); + j++; + } + segments.splice(j, 0, new TextSegment({ + content: points[i], + localGui: args.dir, + isSeparator: true + })); + if (pos + points[i].length + 1 <= content.length) { + segments.splice(j + 1, 0, new TextSegment({ + content: content.substring(pos + points[i].length), + textDirection: args.subDir, + localGui: args.dir, + inPoints: true + })); + } + } + } + } + for (i = 0; i < segments.length; i++) { + if (segments[i].keep) { + segments[i].keep = false; + } else if(segments[i].inPoints){ + segments[i].isParsed = true; + segments[i].inPoints = false; + } + } + return segments; + } + }; + })(); + + var common = (function() { + return { + handle: function (content, segments, args, locale) { + var cases = []; + if (Array.isArray(args.cases)) { + cases = args.cases; + } + var points = []; + if (typeof(args.points) !== "undefined") { + if (Array.isArray(args.points)) { + points = args.points; + } else if (typeof(args.points) === "string") { + points = args.points.split(""); + } + } + var subs = {}; + if (typeof(args.subs) === "object") { + subs = args.subs; + } + var aBounds = []; + if (Array.isArray(args.bounds)) { + aBounds = args.bounds; + } + + tools.handleBounds(segments, args, aBounds, content, locale); + tools.handleSubcontents(segments, args, subs, content, locale); + tools.handleCases(segments, args, cases, content, locale); + tools.handlePoints(segments, args, points, content, locale); + return segments; + } + }; + })(); + + var misc = (function() { + var isBidiLocale = function (locale) { + var lang = !locale ? "" : locale.split("-")[0]; + if (!lang || lang.length < 2) { + return false; + } + return ["iw", "he", "ar", "fa", "ur"].some(function (bidiLang) { + return bidiLang === lang; + }); + }; + var LRE = "\u202A"; + var RLE = "\u202B"; + var PDF = "\u202C"; + var LRM = "\u200E"; + var RLM = "\u200F"; + var LRO = "\u202D"; + var RLO = "\u202E"; + + return { + LRE: LRE, + RLE: RLE, + PDF: PDF, + LRM: LRM, + RLM: RLM, + LRO: LRO, + RLO: RLO, + + getLocaleDetails: function (locale) { + if (!locale) { + locale = typeof navigator === "undefined" ? "" : + (navigator.language || + navigator.userLanguage || + ""); + } + locale = locale.toLowerCase(); + if (isBidiLocale(locale)) { + var full = locale.split("-"); + return {lang: full[0], country: full[1] ? full[1] : ""}; + } + return {lang: "not-bidi"}; + }, + + removeUcc: function (text) { + if (text) { + return text.replace(/[\u200E\u200F\u202A-\u202E]/g, ""); + } + return text; + }, + + removeTags: function (text) { + if (text) { + return text.replace(/<[^<]*>/g, ""); + } + return text; + }, + + getDirection: function (text, dir, guiDir, checkEnd) { + if (dir !== "auto" && (/^(rtl|ltr)$/i).test(dir)) { + return dir; + } + guiDir = (/^(rtl|ltr)$/i).test(guiDir) ? guiDir : "ltr"; + var txt = !checkEnd ? text : text.split("").reverse().join(""); + var fdc = /[A-Za-z\u05d0-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(txt); + return fdc ? (fdc[0] <= "z" ? "ltr" : "rtl") : guiDir; + }, + + hasArabicChar: function (text) { + var fdc = /[\u0600-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(text); + return !!fdc; + }, + + showMarks: function (text, guiDir) { + var result = ""; + for (var i = 0; i < text.length; i++) { + var c = "" + text.charAt(i); + switch (c) { + case LRM: + result += ""; + break; + case RLM: + result += ""; + break; + case LRE: + result += ""; + break; + case RLE: + result += ""; + break; + case LRO: + result += ""; + break; + case RLO: + result += ""; + break; + case PDF: + result += ""; + break; + default: + result += c; + } + } + var mark = typeof(guiDir) === "undefined" || !((/^(rtl|ltr)$/i).test(guiDir)) ? "" : + guiDir === "rtl" ? RLO : LRO; + return mark + result + (mark === "" ? "" : PDF); + }, + + hideMarks: function (text) { + var txt = text.replace(//g, this.LRM).replace(//g, this.RLM).replace(//g, this.LRE); + return txt.replace(//g, this.RLE).replace(//g, this.LRO).replace(//g, this.RLO).replace(//g, this.PDF); + }, + + showTags: function (text) { + return "" + text + ""; + }, + + hideTags: function (text) { + return text.replace(//g,"").replace(/<\/xmp>/g,""); + } + }; + })(); + + var stext = (function() { + var stt = {}; + + // args + // handler: main handler (default - dbidi/stt/handlers/common) + // guiDir: GUI direction (default - "ltr") + // dir: main stt direction (default - guiDir) + // subDir: direction of subsegments + // points: array of delimiters (default - []) + // bounds: array of definitions of bounds in which handler works + // subs: object defines special handling for some substring if found + // cases: array of additional modules with their args for handling special cases (default - []) + function parseAndDisplayStructure(content, fArgs, isHtml, locale) { + if (!content || !fArgs) { + return content; + } + return displayStructure(parseStructure(content, fArgs, locale), fArgs, isHtml); + } + + function checkArguments(fArgs, fullCheck) { + var args = Array.isArray(fArgs)? fArgs[0] : fArgs; + if (!args.guiDir) { + args.guiDir = "ltr"; + } + if (!args.dir) { + args.dir = args.guiDir; + } + if (!fullCheck) { + return args; + } + if (typeof(args.points) === "undefined") { + args.points = []; + } + if (!args.cases) { + args.cases = []; + } + if (!args.bounds) { + args.bounds = []; + } + args.commonHandler = common; + return args; + } + + function parseStructure(content, fArgs, locale) { + if (!content || !fArgs) { + return new TextSegment({content: ""}); + } + var args = checkArguments(fArgs, true); + var segments = [new TextSegment( + { + content: content, + actual: content, + localGui: args.dir + })]; + var parse = common.handle; + if (args.handler && typeof(args.handler) === "function") { + parse = args.handler.handle; + } + parse(content, segments, args, locale); + return segments; + } + + function displayStructure(segments, fArgs, isHtml) { + var args = checkArguments(fArgs, false); + if (isHtml) { + return getResultWithHtml(segments, args); + } + else { + return getResultWithUcc(segments, args); + } + } + + function getResultWithUcc(segments, args, isHtml) { + var result = ""; + var checkedDir = ""; + var prevDir = ""; + var stop = false; + for (var i = 0; i < segments.length; i++) { + if (segments[i].isVisible) { + var dir = segments[i].textDirection; + var lDir = segments[i].localGui; + if (lDir !== "" && prevDir === "") { + result += (lDir === "rtl" ? misc.RLE : misc.LRE); + } + else if(prevDir !== "" && (lDir === "" || lDir !== prevDir || stop)) { + result += misc.PDF + (i == segments.length - 1 && lDir !== ""? "" : args.dir === "rtl" ? misc.RLM : misc.LRM); + if (lDir !== "") { + result += (lDir === "rtl" ? misc.RLE : misc.LRE); + } + } + if (dir === "auto") { + dir = misc.getDirection(segments[i].content, dir, args.guiDir); + } + if ((/^(rtl|ltr)$/i).test(dir)) { + result += (dir === "rtl" ? misc.RLE : misc.LRE) + segments[i].content + misc.PDF; + checkedDir = dir; + } + else { + result += segments[i].content; + checkedDir = misc.getDirection(segments[i].content, dir, args.guiDir, true); + } + if (i < segments.length - 1) { + var locDir = lDir && segments[i+1].localGui? lDir : args.dir; + result += locDir === "rtl" ? misc.RLM : misc.LRM; + } + else if(prevDir !== "") { + result += misc.PDF; + } + prevDir = lDir; + stop = false; + } + else { + stop = true; + } + } + var sttDir = args.dir === "auto" ? misc.getDirection(segments[0].actual, args.dir, args.guiDir) : args.dir; + if (sttDir !== args.guiDir) { + result = (sttDir === "rtl" ? misc.RLE : misc.LRE) + result + misc.PDF; + } + return result; + } + + function getResultWithHtml(segments, args, isHtml) { + var result = ""; + var checkedDir = ""; + var prevDir = ""; + for (var i = 0; i < segments.length; i++) { + if (segments[i].isVisible) { + var dir = segments[i].textDirection; + var lDir = segments[i].localGui; + if (lDir !== "" && prevDir === "") { + result += "<bdi dir='" + (lDir === "rtl" ? "rtl" : "ltr") + "'>"; + } + else if(prevDir !== "" && (lDir === "" || lDir !== prevDir || stop)) { + result += "</bdi>" + (i == segments.length - 1 && lDir !== ""? "" : "<span style='unicode-bidi: embed; direction: " + (args.dir === "rtl" ? "rtl" : "ltr") + ";'></span>"); + if (lDir !== "") { + result += "<bdi dir='" + (lDir === "rtl" ? "rtl" : "ltr") + "'>"; + } + } + + if (dir === "auto") { + dir = misc.getDirection(segments[i].content, dir, args.guiDir); + } + if ((/^(rtl|ltr)$/i).test(dir)) { + //result += "<span style='unicode-bidi: embed; direction: " + (dir === "rtl" ? "rtl" : "ltr") + ";'>" + segments[i].content + "</span>"; + result += "<bdi dir='" + (dir === "rtl" ? "rtl" : "ltr") + "'>" + segments[i].content + "</bdi>"; + checkedDir = dir; + } + else { + result += segments[i].content; + checkedDir = misc.getDirection(segments[i].content, dir, args.guiDir, true); + } + if (i < segments.length - 1) { + var locDir = lDir && segments[i+1].localGui? lDir : args.dir; + result += "<span style='unicode-bidi: embed; direction: " + (locDir === "rtl" ? "rtl" : "ltr") + ";'></span>"; + } + else if(prevDir !== "") { + result += "</bdi>"; + } + prevDir = lDir; + stop = false; + } + else { + stop = true; + } + } + var sttDir = args.dir === "auto" ? misc.getDirection(segments[0].actual, args.dir, args.guiDir) : args.dir; + if (sttDir !== args.guiDir) { + result = "<bdi dir='" + (sttDir === "rtl" ? "rtl" : "ltr") + "'>" + result + "</bdi>"; + } + return result; + } + + //TBD ? + function restore(text, isHtml) { + return text; + } + + stt.parseAndDisplayStructure = parseAndDisplayStructure; + stt.parseStructure = parseStructure; + stt.displayStructure = displayStructure; + stt.restore = restore; + + return stt; + })(); + + var breadcrumb = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: args.dir ? args.dir : isRtl ? "rtl" : "ltr", + subs: { + content: ">", + continued: true, + subDir: isRtl ? "rtl" : "ltr" + }, + cases: [{ + args: { + subs: { + content: "<", + continued: true, + subDir: isRtl ? "ltr" : "rtl" + } + } + }] + }; + + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var comma = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: "," + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var email = (function() { + function getDir(text, locale) { + if (misc.getLocaleDetails(locale).lang !== "ar") { + return "ltr"; + } + var ind = text.indexOf("@"); + if (ind > 0 && ind < text.length - 1) { + return misc.hasArabicChar(text.substring(ind + 1)) ? "rtl" : "ltr"; + } + return "ltr"; + } + + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: getDir(text, locale), + points: "<>.:,;@", + cases: [{ + handler: common, + args: { + bounds: [{ + startAfter: "\"", + endBefore: "\"" + }, + { + startAfter: "(", + endBefore: ")" + } + ], + points: "" + } + }] + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var filepath = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: "/\\:." + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var formula = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: " /%^&[]<>=!?~:.,|()+-*{}", + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + + var sql = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: "\t!#%&()*+,-./:;<=>?|[]{}", + cases: [{ + handler: common, + args: { + bounds: [{ + startAfter: "/*", + endBefore: "*/" + }, + { + startAfter: "--", + end: "\n" + }, + { + startAfter: "--" + } + ] + } + }, + { + handler: common, + args: { + subs: { + content: " ", + continued: true + } + } + }, + { + handler: common, + args: { + bounds: [{ + startAfter: "'", + endBefore: "'" + }, + { + startAfter: "\"", + endBefore: "\"" + } + ] + } + } + ] + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var underscore = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: "_" + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var url = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: ":?#/@.[]=" + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var word = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: args.dir ? args.dir : isRtl ? "rtl" : "ltr", + points: " ,.!?;:", + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var xpath = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var fArgs = + { + guiDir: isRtl ? "rtl" : "ltr", + dir: "ltr", + points: " /[]<>=!:@.|()+-*", + cases: [{ + handler: common, + args: { + bounds: [{ + startAfter: "\"", + endBefore: "\"" + }, + { + startAfter: "'", + endBefore: "'" + } + ], + points: "" + } + } + ] + }; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, fArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, fArgs, !!isHtml, locale); + } + } + }; + })(); + + var custom = (function() { + return { + format: function (text, args, isRtl, isHtml, locale, parseOnly) { + var hArgs = {}; + var prop = ""; + var sArgs = Array.isArray(args)? args[0] : args; + for (prop in sArgs) { + if (sArgs.hasOwnProperty(prop)) { + hArgs[prop] = sArgs[prop]; + } + } + hArgs.guiDir = isRtl ? "rtl" : "ltr"; + hArgs.dir = hArgs.dir ? hArgs.dir : hArgs.guiDir; + if (!parseOnly) { + return stext.parseAndDisplayStructure(text, hArgs, !!isHtml, locale); + } + else { + return stext.parseStructure(text, hArgs, !!isHtml, locale); + } + } + }; + })(); + + var message = (function() { + var params = {msgLang: "en", msgDir: "", phLang: "", phDir: "", phPacking: ["{","}"], phStt: {type: "none", args: {}}, guiDir: ""}; + var parametersChecked = false; + + function getDirectionOfLanguage(lang) { + if (lang === "he" || lang === "iw" || lang === "ar") { + return "rtl"; + } + return "ltr"; + } + + function checkParameters(obj) { + if (obj.msgDir.length === 0) { + obj.msgDir = getDirectionOfLanguage(obj.msgLang); + } + obj.msgDir = obj.msgDir !== "ltr" && obj.msgDir !== "rtl" && obj.msgDir != "auto"? "ltr" : obj.msgDir; + if (obj.guiDir.length === 0) { + obj.guiDir = obj.msgDir; + } + obj.guiDir = obj.guiDir !== "rtl"? "ltr" : "rtl"; + if (obj.phDir.length === 0) { + obj.phDir = obj.phLang.length === 0? obj.msgDir : getDirectionOfLanguage(obj.phLang); + } + obj.phDir = obj.phDir !== "ltr" && obj.phDir !== "rtl" && obj.phDir != "auto"? "ltr" : obj.phDir; + if (typeof (obj.phPacking) === "string") { + obj.phPacking = obj.phPacking.split(""); + } + if (obj.phPacking.length < 2) { + obj.phPacking = ["{","}"]; + } + } + + return { + setDefaults: function (args) { + for (var prop in args) { + if (params.hasOwnProperty(prop)) { + params[prop] = args[prop]; + } + } + checkParameters(params); + parametersChecked = true; + }, + + format: function (text) { + if (!parametersChecked) { + checkParameters(params); + parametersChecked = true; + } + var isHtml = false; + var hasHtmlArg = false; + var spLength = params.phPacking[0].length; + var epLength = params.phPacking[1].length; + if (arguments.length > 0) { + var last = arguments[arguments.length-1]; + if (typeof (last) === "boolean") { + isHtml = last; + hasHtmlArg = true; + } + } + //Message + var re = new RegExp(params.phPacking[0] + "\\d+" + params.phPacking[1]); + var m; + var tSegments = []; + var offset = 0; + var txt = text; + while ((m = re.exec(txt)) != null) { + var lastIndex = txt.indexOf(m[0]) + m[0].length; + if (lastIndex > m[0].length) { + tSegments.push({text: txt.substring(0, lastIndex - m[0].length), ph: false}); + } + tSegments.push({text: m[0], ph: true}); + offset += lastIndex; + txt = txt.substring(lastIndex, txt.length); + } + if (offset < text.length) { + tSegments.push({text: text.substring(offset, text.length), ph: false}); + } + //Parameters + var tArgs = []; + for (var i = 1; i < arguments.length - (hasHtmlArg? 1 : 0); i++) { + var arg = arguments[i]; + var checkArr = arg; + var inLoop = false; + var indArr = 0; + if (Array.isArray(checkArr)) { + arg = checkArr[0]; + if (typeof(arg) === "undefined") { + continue; + } + inLoop = true; + } + do { + if (typeof (arg) === "string") { + tArgs.push({text: arg, dir: params.phDir, stt: params.stt}); + } + else if(typeof (arg) === "boolean") { + isHtml = arg; + } + else if(typeof (arg) === "object") { + tArgs.push(arg); + if (!arg.hasOwnProperty("text")) { + tArgs[tArgs.length-1].text = "{???}"; + } + if (!arg.hasOwnProperty("dir") || arg.dir.length === 0) { + tArgs[tArgs.length-1].dir = params.phDir; + } + if (!arg.hasOwnProperty("stt") || (typeof (arg.stt) === "string" && arg.stt.length === 0) || + (typeof (arg.stt) === "object" && Object.keys(arg.stt).length === 0)) { + tArgs[tArgs.length-1].stt = params.phStt; + } + } + else { + tArgs.push({text: "" + arg, dir: params.phDir, stt: params.phStt}); + } + if (inLoop) { + indArr++; + if (indArr == checkArr.length) { + inLoop = false; + } + else { + arg = checkArr[indArr]; + } + } + } while(inLoop); + } + //Indexing + var segments = []; + for (i = 0; i < tSegments.length; i++) { + var t = tSegments[i]; + if (!t.ph) { + segments.push(new TextSegment({content: t.text, textDirection: params.msgDir})); + } + else { + var ind = parseInt(t.text.substring(spLength, t.text.length - epLength)); + if (isNaN(ind) || ind >= tArgs.length) { + segments.push(new TextSegment({content: t.text, textDirection: params.msgDir})); + continue; + } + var sttType = "none"; + if (!tArgs[ind].stt) { + tArgs[ind].stt = params.phStt; + } + if (tArgs[ind].stt) { + if (typeof (tArgs[ind].stt) === "string") { + sttType = tArgs[ind].stt; + } + else if(tArgs[ind].stt.hasOwnProperty("type")) { + sttType = tArgs[ind].stt.type; + } + } + if (sttType.toLowerCase() !== "none") { + var sttSegs = getHandler(sttType).format(tArgs[ind].text, tArgs[ind].stt.args || {}, + params.msgDir === "rtl", false, params.msgLang, true); + for (var j = 0; j < sttSegs.length; j++) { + segments.push(sttSegs[j]); + } + segments.push(new TextSegment({isVisible: false})); + } + else { + segments.push(new TextSegment({content: tArgs[ind].text, textDirection: (tArgs[ind].dir? tArgs[ind].dir : params.phDir)})); + } + } + } + var result = stext.displayStructure(segments, {guiDir: params.guiDir, dir: params.msgDir}, isHtml); + return result; + } + }; + })(); + + var event = null; + + function getHandler(type) { + switch (type) { + case "breadcrumb" : + return breadcrumb; + case "comma" : + return comma; + case "email" : + return email; + case "filepath" : + return filepath; + case "formula" : + return formula; + case "sql" : + return sql; + case "underscore" : + return underscore; + case "url" : + return url; + case "word" : + return word; + case "xpath" : + return xpath; + default: + return custom; + } + } + + function isInputEventSupported(element) { + var agent = window.navigator.userAgent; + if (agent.indexOf("MSIE") >=0 || agent.indexOf("Trident") >=0 || agent.indexOf("Edge") >=0) { + return false; + } + var checked = document.createElement(element.tagName); + checked.contentEditable = true; + var isSupported = ("oninput" in checked); + if (!isSupported) { + checked.setAttribute('oninput', 'return;'); + isSupported = typeof checked['oninput'] == 'function'; + } + checked = null; + return isSupported; + } + + function attachElement(element, type, args, isRtl, locale) { + //if (!element || element.nodeType != 1 || !element.isContentEditable) + if (!element || element.nodeType != 1) { + return false; + } + if (!event) { + event = document.createEvent('Event'); + event.initEvent('TF', true, true); + } + element.setAttribute("data-tf-type", type); + var sArgs = args === "undefined"? "{}" : JSON.stringify(Array.isArray(args)? args[0] : args); + element.setAttribute("data-tf-args", sArgs); + var dir = "ltr"; + if (isRtl === "undefined") { + if (element.dir) { + dir = element.dir; + } + else if(element.style && element.style.direction) { + dir = element.style.direction; + } + isRtl = dir.toLowerCase() === "rtl"; + } + element.setAttribute("data-tf-dir", isRtl); + element.setAttribute("data-tf-locale", misc.getLocaleDetails(locale).lang); + if (isInputEventSupported(element)) { + var ehandler = element.oninput; + element.oninput = function(event) { + displayWithStructure(event.target); + }; + } + else { + element.onkeyup = function(e) { + displayWithStructure(e.target); + element.dispatchEvent(event); + }; + element.onmouseup = function(e) { + displayWithStructure(e.target); + element.dispatchEvent(event); + }; + } + displayWithStructure(element); + + return true; + } + + function detachElement(element) { + if (!element || element.nodeType != 1) { + return; + } + element.removeAttribute("data-tf-type"); + element.removeAttribute("data-tf-args"); + element.removeAttribute("data-tf-dir"); + element.removeAttribute("data-tf-locale"); + element.innerHTML = element.textContent || ""; + } + + function displayWithStructure(element) { + var txt = element.textContent || ""; + if (txt.length === 0) { + element.dispatchEvent(event); + return; + } + var selection = document.getSelection(); + var range = selection.getRangeAt(0); + var tempRange = range.cloneRange(), startNode, startOffset; + startNode = range.startContainer; + startOffset = range.startOffset; + var textOffset = 0; + if (startNode.nodeType === 3) { + textOffset += startOffset; + } + tempRange.setStart(element,0); + tempRange.setEndBefore(startNode); + var div = document.createElement('div'); + div.appendChild(tempRange.cloneContents()); + textOffset += div.textContent.length; + + element.innerHTML = getHandler(element.getAttribute("data-tf-type")). + format(txt, JSON.parse(element.getAttribute("data-tf-args")), (element.getAttribute("data-tf-dir") === "true"? true : false), + true, element.getAttribute("data-tf-locale")); + var parent = element; + var node = element; + var newOffset = 0; + var inEnd = false; + selection.removeAllRanges(); + range.setStart(element,0); + range.setEnd(element,0); + while (node) { + if (node.nodeType === 3) { + if (newOffset + node.nodeValue.length >= textOffset) { + range.setStart(node, textOffset - newOffset); + break; + } + else { + newOffset += node.nodeValue.length; + node = node.nextSibling; + } + } + else if(node.hasChildNodes()) { + parent = node; + node = parent.firstChild; + continue; + } + else { + node = node.nextSibling; + } + while (!node) { + if (parent === element) { + inEnd = true; + break; + } + node = parent.nextSibling; + parent = parent.parentNode; + } + if (inEnd) { + break; + } + } + + selection.addRange(range); + element.dispatchEvent(event); + } + + return { + /** + * Returns the HTML representation of a given structured text + * @param text - the structured text + * @param type - could be one of filepath, url, email + * @param args - pass additional arguments to the handler. generally null. + * @param isRtl - indicates if the GUI is mirrored + * @param locale - the browser locale + */ + getHtml: function (text, type, args, isRtl, locale) { + return getHandler(type).format(text, args, isRtl, true, locale); + }, + /** + * Handle Structured text correct display for a given HTML element. + * @param element - the element : should be of type div contenteditable=true + * @param type - could be one of filepath, url, email + * @param args - pass additional arguments to the handler. generally null. + * @param isRtl - indicates if the GUI is mirrored + * @param locale - the browser locale + */ + attach: function (element, type, args, isRtl, locale) { + return attachElement(element, type, args, isRtl, locale); + } + }; +})(); diff --git a/editor/js/history.js b/editor/js/history.js index 265c365bf..74520beac 100644 --- a/editor/js/history.js +++ b/editor/js/history.js @@ -236,7 +236,7 @@ RED.history = (function() { }); if (ev.node.type === 'subflow') { - $("#menu-item-workspace-menu-"+ev.node.id.replace(".","-")).text(ev.node.name); + $("#menu-item-workspace-menu-"+ev.node.id.replace(".","-")).text(RED.bidi.enforceTextDirectionWithUCC(ev.node.name)); } } else { RED.editor.updateNodeProperties(ev.node); diff --git a/editor/js/main.js b/editor/js/main.js index 3e1b55761..b51fa9047 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -1,5 +1,5 @@ /** - * Copyright 2013, 2015 IBM Corp. + * Copyright 2013, 2016 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -176,7 +176,13 @@ var RED = (function() { {id:"menu-item-view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:RED.view.toggleSnapGrid}, {id:"menu-item-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:toggleStatus, selected: true}, null, - {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true} + {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true}, + {id:"menu-item-bidi",label:RED._("menu.label.view.textDir"),options:[ + {id:"menu-item-bidi-default",toggle:"text-direction",label:RED._("menu.label.view.defaultDir"),selected: true, onselect:function(s) { if(s){RED.view.toggleTextDir("")}}}, + {id:"menu-item-bidi-ltr",toggle:"text-direction",label:RED._("menu.label.view.ltr"), onselect:function(s) { if(s){RED.view.toggleTextDir("ltr")}}}, + {id:"menu-item-bidi-rtl",toggle:"text-direction",label:RED._("menu.label.view.rtl"), onselect:function(s) { if(s){RED.view.toggleTextDir("rtl")}}}, + {id:"menu-item-bidi-auto",toggle:"text-direction",label:RED._("menu.label.view.auto"), onselect:function(s) { if(s){RED.view.toggleTextDir("auto")}}} + ]} ]}, null, {id:"menu-item-import",label:RED._("menu.label.import"),options:[ diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index c9f19578d..fc638dde4 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -295,17 +295,25 @@ RED.editor = (function() { * @param node - the node being edited * @param property - the name of the field * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) + * @param definition - the definition of the field */ - function preparePropertyEditor(node,property,prefix) { + function preparePropertyEditor(node,property,prefix,definition) { var input = $("#"+prefix+"-"+property); if (input.attr('type') === "checkbox") { input.prop('checked',node[property]); - } else { + } + else { var val = node[property]; if (val == null) { val = ""; } - input.val(val); + if ("format" in definition[property] && definition[property].format !== "" && input[0].nodeName === "DIV") { + input.html(RED.format.getHtml(val, definition[property].format, {}, false, "en")); + RED.format.attach(input[0], definition[property].format, {}, false, "en"); + } else { + input.val(val).attr("dir", RED.bidi.resolveBaseTextDir(val)); + RED.bidi.initInputEvents(input); + } } } @@ -1197,7 +1205,7 @@ RED.editor = (function() { changes['name'] = editing_node.name; editing_node.name = newName; changed = true; - $("#menu-item-workspace-menu-"+editing_node.id.replace(".","-")).text(newName); + $("#menu-item-workspace-menu-"+editing_node.id.replace(".","-")).text(RED.bidi.enforceTextDirectionWithUCC(newName)); } var newDescription = subflowEditor.getValue(); @@ -1289,7 +1297,8 @@ RED.editor = (function() { value: "" }); - $("#subflow-input-name").val(subflow.name); + $("#subflow-input-name").val(subflow.name).attr("dir", RED.bidi.resolveBaseTextDir(subflow.name)); + RED.bidi.initInputEvents($("#subflow-input-name")); subflowEditor.getSession().setValue(subflow.info||"",-1); var userCount = 0; var subflowType = "subflow:"+editing_node.id; diff --git a/editor/js/ui/palette.js b/editor/js/ui/palette.js index f17889ded..236a0c612 100644 --- a/editor/js/ui/palette.js +++ b/editor/js/ui/palette.js @@ -91,15 +91,15 @@ RED.palette = (function() { el.css({height:multiLineNodeHeight+"px"}); var labelElement = el.find(".palette_label"); - labelElement.html(lines); + labelElement.html(lines).attr('dir', RED.bidi.resolveBaseTextDir(lines)); el.find(".palette_port").css({top:(multiLineNodeHeight/2-5)+"px"}); var popOverContent; try { - var l = "<p><b>"+label+"</b></p>"; + var l = "<p><b>"+RED.bidi.enforceTextDirectionWithUCC(label)+"</b></p>"; if (label != type) { - l = "<p><b>"+label+"</b><br/><i>"+type+"</i></p>"; + l = "<p><b>"+RED.bidi.enforceTextDirectionWithUCC(label)+"</b><br/><i>"+type+"</i></p>"; } popOverContent = $(l+(info?info:$("script[data-help-name|='"+type+"']").html()||"<p>"+RED._("palette.noInfo")+"</p>").trim()) .filter(function(n) { diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index c11e1a446..11702e74d 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -70,7 +70,7 @@ RED.sidebar.info = (function() { var table = '<table class="node-info"><tbody>'; table += '<tr class="blank"><td colspan="2">'+RED._("sidebar.info.node")+'</td></tr>'; if (node.type != "subflow" && node.name) { - table += "<tr><td>"+RED._("common.label.name")+"</td><td>&nbsp;"+node.name+"</td></tr>"; + table += '<tr><td>'+RED._("common.label.name")+'</td><td>&nbsp;<span class="bidiAware" dir="'+RED.bidi.resolveBaseTextDir(node.name)+'">'+node.name+'</span></td></tr>'; } table += "<tr><td>"+RED._("sidebar.info.type")+"</td><td>&nbsp;"+node.type+"</td></tr>"; table += "<tr><td>"+RED._("sidebar.info.id")+"</td><td>&nbsp;"+node.id+"</td></tr>"; @@ -93,7 +93,7 @@ RED.sidebar.info = (function() { userCount++; } }); - table += "<tr><td>"+RED._("common.label.name")+"</td><td>"+subflowNode.name+"</td></tr>"; + table += '<tr><td>'+RED._("common.label.name")+'</td><td><span class="bidiAware" dir=\"'+RED.bidi.resolveBaseTextDir(subflowNode.name)+'">'+subflowNode.name+'</span></td></tr>'; table += "<tr><td>"+RED._("sidebar.info.instances")+"</td><td>"+userCount+"</td></tr>"; } @@ -140,13 +140,14 @@ RED.sidebar.info = (function() { table += "</tbody></table><hr/>"; if (!subflowNode && node.type != "comment") { var helpText = $("script[data-help-name|='"+node.type+"']").html()||""; - table += '<div class="node-help">'+helpText+"</div>"; + table += '<div class="node-help"><span class="bidiAware" dir=\"'+RED.bidi.resolveBaseTextDir(helpText)+'">'+helpText+'</span></div>'; } if (subflowNode) { - table += '<div class="node-help">'+marked(subflowNode.info||"")+'</div>'; + table += '<div class="node-help"><span class="bidiAware" dir=\"'+RED.bidi.resolveBaseTextDir(subflowNode.info||"")+'">'+marked(subflowNode.info||"")+'</span></div>'; } else if (node._def && node._def.info) { var info = node._def.info; - table += '<div class="node-help">'+marked(typeof info === "function" ? info.call(node) : info)+'</div>'; + var textInfo = (typeof info === "function" ? info.call(node) : info); + table += '<div class="node-help"><span class="bidiAware" dir=\"'+RED.bidi.resolveBaseTextDir(textInfo)+'">'+marked(textInfo)+'</span></div>'; //table += '<div class="node-help">'+(typeof info === "function" ? info.call(node) : info)+'</div>'; } diff --git a/editor/js/ui/tabs.js b/editor/js/ui/tabs.js index 1b98c117a..2a7a6c8d9 100644 --- a/editor/js/ui/tabs.js +++ b/editor/js/ui/tabs.js @@ -126,7 +126,8 @@ RED.tabs = (function() { if (tab.icon) { $('<img src="'+tab.icon+'" class="red-ui-tab-icon"/>').appendTo(link); } - $('<span/>').text(tab.label).appendTo(link); + var span = $('<span/>',{class:"bidiAware"}).text(tab.label).appendTo(link); + span.attr('dir', RED.bidi.resolveBaseTextDir(tab.label)); link.on("click",onTabClick); link.on("dblclick",onTabDblClick); @@ -239,7 +240,7 @@ RED.tabs = (function() { tabs[id].label = label; var tab = ul.find("a[href='#"+id+"']"); tab.attr("title",label); - tab.find("span").text(label); + tab.find("span").text(label).attr('dir', RED.bidi.resolveBaseTextDir(label)); updateTabWidths(); }, order: function(order) { diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 36289c218..920d77feb 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -1792,6 +1792,7 @@ RED.view = (function() { l = d._def.label; try { l = (typeof l === "function" ? l.call(d) : l)||""; + l = RED.bidi.enforceTextDirectionWithUCC(l); } catch(err) { console.log("Definition error: "+d.type+".label",err); l = d.type; @@ -2154,7 +2155,8 @@ RED.view = (function() { ).classed("link_selected", false); } - + RED.bidi.enforceTextDirectionOnPage(); + if (d3.event) { d3.event.preventDefault(); } @@ -2357,6 +2359,12 @@ RED.view = (function() { toggleSnapGrid: function(state) { snapGrid = state; redraw(); + }, + toggleTextDir: function(value) { + RED.bidi.setTextDirection(value); + RED.nodes.eachNode(function(n) { n.dirty = true;}); + redraw(); + RED.palette.refresh(); }, scale: function() { return scaleFactor; diff --git a/editor/js/ui/workspaces.js b/editor/js/ui/workspaces.js index 8cf0e5ce2..f41393232 100644 --- a/editor/js/ui/workspaces.js +++ b/editor/js/ui/workspaces.js @@ -94,7 +94,7 @@ RED.workspaces = (function() { workspace_tabs.renameTab(workspace.id,label); RED.nodes.dirty(true); RED.sidebar.config.refresh(); - $("#menu-item-workspace-menu-"+workspace.id.replace(".","-")).text(label); + $("#menu-item-workspace-menu-"+workspace.id.replace(".","-")).text(RED.bidi.enforceTextDirectionWithUCC(label)); } RED.tray.close(); } @@ -109,7 +109,8 @@ RED.workspaces = (function() { '</div>').appendTo(dialogForm); $('<input type="text" style="display: none;" />').prependTo(dialogForm); dialogForm.submit(function(e) { e.preventDefault();}); - $("#node-input-name").val(workspace.label); + $("#node-input-name").val(workspace.label).attr("dir", RED.bidi.resolveBaseTextDir(workspace.label)); + RED.bidi.initInputEvents($("#node-input-name")); dialogForm.i18n(); }, close: function() { @@ -225,7 +226,7 @@ RED.workspaces = (function() { refresh: function() { RED.nodes.eachWorkspace(function(ws) { workspace_tabs.renameTab(ws.id,ws.label); - $("#menu-item-workspace-menu-"+ws.id.replace(".","-")).text(ws.label); + $("#menu-item-workspace-menu-"+ws.id.replace(".","-")).text(RED.bidi.enforceTextDirectionWithUCC(ws.label)); }) RED.nodes.eachSubflow(function(sf) { diff --git a/editor/sass/editor.scss b/editor/sass/editor.scss index f647bff16..52cf2b428 100644 --- a/editor/sass/editor.scss +++ b/editor/sass/editor.scss @@ -196,7 +196,7 @@ display: inline-block; width: 100px; } -.form-row input { +.form-row input .form-row div[contenteditable="true"] { width:70%; } diff --git a/editor/sass/forms.scss b/editor/sass/forms.scss index 442c516a2..70d86664b 100644 --- a/editor/sass/forms.scss +++ b/editor/sass/forms.scss @@ -26,6 +26,7 @@ button, input, select, +div[contenteditable="true"], textarea { margin: 0; font-size: 100%; @@ -33,12 +34,14 @@ textarea { } button, +div[contenteditable="true"], input { *overflow: visible; line-height: normal; } button::-moz-focus-inner, +div[contenteditable="true"]::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; @@ -110,6 +113,7 @@ legend small { label, input, +div[contenteditable="true"], button, select, textarea { @@ -119,6 +123,7 @@ textarea { } input, +div[contenteditable="true"], button, select, textarea { @@ -161,6 +166,7 @@ input[type="color"], input, textarea, +div[contenteditable="true"], .uneditable-input { width: 206px; } @@ -184,6 +190,7 @@ input[type="url"], input[type="search"], input[type="tel"], input[type="color"], +div[contenteditable="true"], .uneditable-input { background-color: #ffffff; border: 1px solid $form-input-border-color; @@ -207,6 +214,7 @@ input[type="url"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, +div[contenteditable="true"], .uneditable-input:focus { border-color: $form-input-focus-color; outline: 0; @@ -294,11 +302,13 @@ textarea:-moz-placeholder { } input:-ms-input-placeholder, +div[contenteditable="true"]:-ms-input-placeholder, textarea:-ms-input-placeholder { color: $form-placeholder-color; } input::-webkit-input-placeholder, +div[contenteditable="true"]::-webkit-input-placeholder, textarea::-webkit-input-placeholder { color: $form-placeholder-color; } @@ -384,6 +394,7 @@ textarea[class*="span"], input, textarea, +div[contenteditable="true"], .uneditable-input { margin-left: 0; } @@ -515,12 +526,14 @@ input[type="checkbox"][readonly] { .control-group.warning .checkbox, .control-group.warning .radio, .control-group.warning input, +.control-group.warning div[contenteditable="true"], .control-group.warning select, .control-group.warning textarea { color: #c09853; } .control-group.warning input, +.control-group.warning div[contenteditable="true"], .control-group.warning select, .control-group.warning textarea { border-color: #c09853; @@ -530,6 +543,7 @@ input[type="checkbox"][readonly] { } .control-group.warning input:focus, +.control-group.warning div[contenteditable="true"]:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { border-color: #a47e3c; @@ -554,12 +568,14 @@ input[type="checkbox"][readonly] { .control-group.error .checkbox, .control-group.error .radio, .control-group.error input, +.control-group.error div[contenteditable="true"], .control-group.error select, .control-group.error textarea { color: #b94a48; } .control-group.error input, +.control-group.error div[contenteditable="true"], .control-group.error select, .control-group.error textarea { border-color: #b94a48; @@ -569,6 +585,7 @@ input[type="checkbox"][readonly] { } .control-group.error input:focus, +.control-group.error div[contenteditable="true"]:focus, .control-group.error select:focus, .control-group.error textarea:focus { border-color: #953b39; @@ -593,12 +610,14 @@ input[type="checkbox"][readonly] { .control-group.success .checkbox, .control-group.success .radio, .control-group.success input, +.control-group.success div[contenteditable="true"], .control-group.success select, .control-group.success textarea { color: #468847; } .control-group.success input, +.control-group.success div[contenteditable="true"], .control-group.success select, .control-group.success textarea { border-color: #468847; @@ -608,6 +627,7 @@ input[type="checkbox"][readonly] { } .control-group.success input:focus, +.control-group.success div[contenteditable="true"]:focus, .control-group.success select:focus, .control-group.success textarea:focus { border-color: #356635; @@ -632,12 +652,14 @@ input[type="checkbox"][readonly] { .control-group.info .checkbox, .control-group.info .radio, .control-group.info input, +.control-group.info div[contenteditable="true"], .control-group.info select, .control-group.info textarea { color: #3a87ad; } .control-group.info input, +.control-group.info div[contenteditable="true"], .control-group.info select, .control-group.info textarea { border-color: #3a87ad; @@ -647,6 +669,7 @@ input[type="checkbox"][readonly] { } .control-group.info input:focus, +.control-group.info div[contenteditable="true"]:focus, .control-group.info select:focus, .control-group.info textarea:focus { border-color: #2d6987; @@ -663,6 +686,7 @@ input[type="checkbox"][readonly] { } input:focus:invalid, +div[contenteditable="true"]:focus:invalid, textarea:focus:invalid, select:focus:invalid { color: #b94a48; @@ -670,6 +694,7 @@ select:focus:invalid { } input:focus:invalid:focus, +div[contenteditable="true"]:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:focus { border-color: #e9322d; @@ -727,6 +752,8 @@ select:focus:invalid:focus { .input-append input, .input-prepend input, +.input-append div[contenteditable="true"], +.input-prepend div[contenteditable="true"], .input-append select, .input-prepend select, .input-append .uneditable-input, @@ -740,6 +767,8 @@ select:focus:invalid:focus { .input-append input, .input-prepend input, +.input-append div[contenteditable="true"], +.input-prepend div[contenteditable="true"], .input-append select, .input-prepend select, .input-append .uneditable-input, @@ -755,6 +784,8 @@ select:focus:invalid:focus { .input-append input:focus, .input-prepend input:focus, +.input-append div[contenteditable="true"]:focus, +.input-prepend div[contenteditable="true"]:focus, .input-append select:focus, .input-prepend select:focus, .input-append .uneditable-input:focus, @@ -809,6 +840,7 @@ select:focus:invalid:focus { } .input-append input, +.input-append div[contenteditable="true"], .input-append select, .input-append .uneditable-input { -webkit-border-radius: 4px 0 0 4px; @@ -839,6 +871,7 @@ select:focus:invalid:focus { } .input-prepend.input-append input, +.input-prepend.input-append div[contenteditable="true"], .input-prepend.input-append select, .input-prepend.input-append .uneditable-input { -webkit-border-radius: 0; diff --git a/editor/sass/jquery.scss b/editor/sass/jquery.scss index 324552ad2..a57db8a04 100644 --- a/editor/sass/jquery.scss +++ b/editor/sass/jquery.scss @@ -18,11 +18,11 @@ font-size: 14px !important; font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif !important; } -.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { +.ui-widget input, .ui-widget div[contenteditable="true"], .ui-widget select, .ui-widget textarea, .ui-widget button { font-size: 14px !important; font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif !important; } -.ui-widget input { +.ui-widget input, .ui-widget div[contenteditable="true"] { box-shadow: none; } diff --git a/nodes/core/io/23-watch.html b/nodes/core/io/23-watch.html index 8d987063f..90149fdfe 100644 --- a/nodes/core/io/23-watch.html +++ b/nodes/core/io/23-watch.html @@ -17,7 +17,7 @@ <script type="text/x-red" data-template-name="watch"> <div class="form-row node-input-filename"> <label for="node-input-files"><i class="fa fa-file"></i> <span data-i18n="watch.label.files"></span></label> - <input type="text" id="node-input-files" data-i18n="[placeholder]watch.placeholder.files"> + <div id="node-input-files" contenteditable="true" tabindex="1" data-i18n="[placeholder]watch.placeholder.files"></div> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> @@ -46,7 +46,7 @@ category: 'advanced-input', defaults: { name: {value:""}, - files: {value:"",required:true} + files: {value:"",required:true, format:"filepath"} }, color:"BurlyWood", inputs:0, diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index 75d1dc8ce..4808f2078 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -25,7 +25,12 @@ "view": { "view": "View", "showGrid": "Show grid", - "snapGrid": "Snap to grid" + "snapGrid": "Snap to grid", + "textDir": "Text Direction", + "defaultDir": "Default", + "ltr": "Left-to-right", + "rtl": "Right-to-left", + "auto": "Contextual" }, "sidebar": { "show": "Show sidebar"