Best JavaScript code snippet using testcafe
jsoneditor.js
Source:jsoneditor.js
...804 var ctrlKey = event.ctrlKey;805 var shiftKey = event.shiftKey;806 var handled = false;807 if (keynum == 9) { // Tab or Shift+Tab808 // FIXME: selecting all text on tab key does not work on IE8 (-> put selectContentEditable() in keyup too?)809 //Node.select(TreeEditor.domFocus);810 setTimeout(function () {811 // select all text when moving focus to an editable div812 util.selectContentEditable(TreeEditor.domFocus);813 }, 0);814 }815 if (this.searchBox) {816 if (ctrlKey && keynum == 70) { // Ctrl+F817 this.searchBox.dom.search.focus();818 this.searchBox.dom.search.select();819 handled = true;820 }821 else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G822 var focus = true;823 if (!shiftKey) {824 // select next search result (F3 or Ctrl+G)825 this.searchBox.next(focus);826 }827 else {828 // select previous search result (Shift+F3 or Ctrl+Shift+G)829 this.searchBox.previous(focus);830 }831 handled = true;832 }833 }834 if (this.history) {835 if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z836 // undo837 this._onUndo();838 handled = true;839 }840 else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z841 // redo842 this._onRedo();843 handled = true;844 }845 }846 if (handled) {847 util.preventDefault(event);848 util.stopPropagation(event);849 }850};851/**852 * Create main table853 * @private854 */855TreeEditor.prototype._createTable = function () {856 var contentOuter = document.createElement('div');857 contentOuter.className = 'outer';858 this.contentOuter = contentOuter;859 this.content = document.createElement('div');860 this.content.className = 'tree';861 contentOuter.appendChild(this.content);862 this.table = document.createElement('table');863 this.table.className = 'tree';864 this.content.appendChild(this.table);865 // IE8 does not handle overflow='auto' correctly.866 // Therefore, set overflow to 'scroll'867 var ieVersion = util.getInternetExplorerVersion();868 if (ieVersion == 8) {869 this.content.style.overflow = 'scroll';870 }871 // create colgroup where the first two columns don't have a fixed872 // width, and the edit columns do have a fixed width873 var col;874 this.colgroupContent = document.createElement('colgroup');875 if (this.mode.edit) {876 col = document.createElement('col');877 col.width = "24px";878 this.colgroupContent.appendChild(col);879 }880 col = document.createElement('col');881 col.width = "24px";882 this.colgroupContent.appendChild(col);883 col = document.createElement('col');884 this.colgroupContent.appendChild(col);885 this.table.appendChild(this.colgroupContent);886 this.tbody = document.createElement('tbody');887 this.table.appendChild(this.tbody);888 this.frame.appendChild(contentOuter);889};890// register modes at the JSONEditor891JSONEditor.modes.tree = {892 editor: TreeEditor,893 data: 'json'894};895JSONEditor.modes.view = {896 editor: TreeEditor,897 data: 'json'898};899JSONEditor.modes.form = {900 editor: TreeEditor,901 data: 'json'902};903// Deprecated modes (deprecated since version 2.2.0)904JSONEditor.modes.editor = {905 editor: TreeEditor,906 data: 'json'907};908JSONEditor.modes.viewer = {909 editor: TreeEditor,910 data: 'json'911};912/**913 * Create a TextEditor and attach it to given container914 * @constructor TextEditor915 * @param {Element} container916 * @param {Object} [options] Object with options. available options:917 * {String} mode Available values:918 * "text" (default)919 * or "code".920 * {Number} indentation Number of indentation921 * spaces. 4 by default.922 * {function} change Callback method923 * triggered on change924 * @param {JSON | String} [json] initial contents of the formatter925 */926function TextEditor(container, options, json) {927 if (!(this instanceof TextEditor)) {928 throw new Error('TextEditor constructor called without "new".');929 }930 this._create(container, options, json);931}932/**933 * Create a TextEditor and attach it to given container934 * @constructor TextEditor935 * @param {Element} container936 * @param {Object} [options] See description in constructor937 * @param {JSON | String} [json] initial contents of the formatter938 * @private939 */940TextEditor.prototype._create = function (container, options, json) {941 // check availability of JSON parser (not available in IE7 and older)942 if (typeof(JSON) == 'undefined') {943 throw new Error('Your browser does not support JSON. \n\n' +944 'Please install the newest version of your browser.\n' +945 '(all modern browsers support JSON).');946 }947 // read options948 options = options || {};949 this.options = options;950 if (options.indentation) {951 this.indentation = Number(options.indentation);952 }953 else {954 this.indentation = 4; // number of spaces955 }956 this.mode = (options.mode == 'code') ? 'code' : 'text';957 if (this.mode == 'code') {958 // verify whether Ace editor is available and supported959 if (typeof ace === 'undefined') {960 this.mode = 'text';961 util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +962 'Falling back to plain text editor');963 }964 if (util.getInternetExplorerVersion() == 8) {965 this.mode = 'text';966 util.log('WARNING: Cannot load code editor, Ace is not supported on IE8. ' +967 'Falling back to plain text editor');968 }969 }970 var me = this;971 this.container = container;972 this.dom = {};973 this.editor = undefined; // ace code editor974 this.textarea = undefined; // plain text editor (fallback when Ace is not available)975 this.width = container.clientWidth;976 this.height = container.clientHeight;977 this.frame = document.createElement('div');978 this.frame.className = 'jsoneditor';979 this.frame.onclick = function (event) {980 // prevent default submit action when TextEditor is located inside a form981 util.preventDefault(event);982 };983 // create menu984 this.menu = document.createElement('div');985 this.menu.className = 'menu';986 this.frame.appendChild(this.menu);987 // create format button988 var buttonFormat = document.createElement('button');989 buttonFormat.className = 'format';990 buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';991 this.menu.appendChild(buttonFormat);992 buttonFormat.onclick = function () {993 try {994 me.format();995 }996 catch (err) {997 me._onError(err);998 }999 };1000 // create compact button1001 var buttonCompact = document.createElement('button');1002 buttonCompact.className = 'compact';1003 buttonCompact.title = 'Compact JSON data, remove all whitespaces';1004 this.menu.appendChild(buttonCompact);1005 buttonCompact.onclick = function () {1006 try {1007 me.compact();1008 }1009 catch (err) {1010 me._onError(err);1011 }1012 };1013 // create mode box1014 if (this.options && this.options.modes && this.options.modes.length) {1015 var modeBox = createModeBox(this, this.options.modes, this.options.mode);1016 this.menu.appendChild(modeBox);1017 this.dom.modeBox = modeBox;1018 }1019 this.content = document.createElement('div');1020 this.content.className = 'outer';1021 this.frame.appendChild(this.content);1022 this.container.appendChild(this.frame);1023 if (this.mode == 'code') {1024 this.editorDom = document.createElement('div');1025 this.editorDom.style.height = '100%'; // TODO: move to css1026 this.editorDom.style.width = '100%'; // TODO: move to css1027 this.content.appendChild(this.editorDom);1028 var editor = ace.edit(this.editorDom);1029 editor.setTheme('ace/theme/jsoneditor');1030 editor.setShowPrintMargin(false);1031 editor.setFontSize(13);1032 editor.getSession().setMode('ace/mode/json');1033 editor.getSession().setUseSoftTabs(true);1034 editor.getSession().setUseWrapMode(true);1035 this.editor = editor;1036 var poweredBy = document.createElement('a');1037 poweredBy.appendChild(document.createTextNode('powered by ace'));1038 poweredBy.href = 'http://ace.ajax.org';1039 poweredBy.target = '_blank';1040 poweredBy.className = 'poweredBy';1041 poweredBy.onclick = function () {1042 // TODO: this anchor falls below the margin of the content,1043 // therefore the normal a.href does not work. We use a click event1044 // for now, but this should be fixed.1045 window.open(poweredBy.href, poweredBy.target);1046 };1047 this.menu.appendChild(poweredBy);1048 if (options.change) {1049 // register onchange event1050 editor.on('change', function () {1051 options.change();1052 });1053 }1054 }1055 else {1056 // load a plain text textarea1057 var textarea = document.createElement('textarea');1058 textarea.className = 'text';1059 textarea.spellcheck = false;1060 this.content.appendChild(textarea);1061 this.textarea = textarea;1062 if (options.change) {1063 // register onchange event1064 if (this.textarea.oninput === null) {1065 this.textarea.oninput = function () {1066 options.change();1067 }1068 }1069 else {1070 // oninput is undefined. For IE8-1071 this.textarea.onchange = function () {1072 options.change();1073 }1074 }1075 }1076 }1077 // load initial json object or string1078 if (typeof(json) == 'string') {1079 this.setText(json);1080 }1081 else {1082 this.set(json);1083 }1084};1085/**1086 * Detach the editor from the DOM1087 * @private1088 */1089TextEditor.prototype._delete = function () {1090 if (this.frame && this.container && this.frame.parentNode == this.container) {1091 this.container.removeChild(this.frame);1092 }1093};1094/**1095 * Throw an error. If an error callback is configured in options.error, this1096 * callback will be invoked. Else, a regular error is thrown.1097 * @param {Error} err1098 * @private1099 */1100TextEditor.prototype._onError = function(err) {1101 // TODO: onError is deprecated since version 2.2.0. cleanup some day1102 if (typeof this.onError === 'function') {1103 util.log('WARNING: JSONEditor.onError is deprecated. ' +1104 'Use options.error instead.');1105 this.onError(err);1106 }1107 if (this.options && typeof this.options.error === 'function') {1108 this.options.error(err);1109 }1110 else {1111 throw err;1112 }1113};1114/**1115 * Compact the code in the formatter1116 */1117TextEditor.prototype.compact = function () {1118 var json = util.parse(this.getText());1119 this.setText(JSON.stringify(json));1120};1121/**1122 * Format the code in the formatter1123 */1124TextEditor.prototype.format = function () {1125 var json = util.parse(this.getText());1126 this.setText(JSON.stringify(json, null, this.indentation));1127};1128/**1129 * Set focus to the formatter1130 */1131TextEditor.prototype.focus = function () {1132 if (this.textarea) {1133 this.textarea.focus();1134 }1135 if (this.editor) {1136 this.editor.focus();1137 }1138};1139/**1140 * Resize the formatter1141 */1142TextEditor.prototype.resize = function () {1143 if (this.editor) {1144 var force = false;1145 this.editor.resize(force);1146 }1147};1148/**1149 * Set json data in the formatter1150 * @param {Object} json1151 */1152TextEditor.prototype.set = function(json) {1153 this.setText(JSON.stringify(json, null, this.indentation));1154};1155/**1156 * Get json data from the formatter1157 * @return {Object} json1158 */1159TextEditor.prototype.get = function() {1160 return util.parse(this.getText());1161};1162/**1163 * Get the text contents of the TextEditor1164 * @return {String} jsonText1165 */1166TextEditor.prototype.getText = function() {1167 if (this.textarea) {1168 return this.textarea.value;1169 }1170 if (this.editor) {1171 return this.editor.getValue();1172 }1173 return '';1174};1175/**1176 * Set the text contents of the TextEditor1177 * @param {String} jsonText1178 */1179TextEditor.prototype.setText = function(jsonText) {1180 if (this.textarea) {1181 this.textarea.value = jsonText;1182 }1183 if (this.editor) {1184 this.editor.setValue(jsonText, -1);1185 }1186};1187// register modes at the JSONEditor1188JSONEditor.modes.text = {1189 editor: TextEditor,1190 data: 'text',1191 load: TextEditor.prototype.format1192};1193JSONEditor.modes.code = {1194 editor: TextEditor,1195 data: 'text',1196 load: TextEditor.prototype.format1197};1198/**1199 * @constructor Node1200 * Create a new Node1201 * @param {TreeEditor} editor1202 * @param {Object} [params] Can contain parameters:1203 * {string} field1204 * {boolean} fieldEditable1205 * {*} value1206 * {String} type Can have values 'auto', 'array',1207 * 'object', or 'string'.1208 */1209function Node (editor, params) {1210 /** @type {TreeEditor} */1211 this.editor = editor;1212 this.dom = {};1213 this.expanded = false;1214 if(params && (params instanceof Object)) {1215 this.setField(params.field, params.fieldEditable);1216 this.setValue(params.value, params.type);1217 }1218 else {1219 this.setField('');1220 this.setValue(null);1221 }1222};1223/**1224 * Set parent node1225 * @param {Node} parent1226 */1227Node.prototype.setParent = function(parent) {1228 this.parent = parent;1229};1230/**1231 * Set field1232 * @param {String} field1233 * @param {boolean} [fieldEditable]1234 */1235Node.prototype.setField = function(field, fieldEditable) {1236 this.field = field;1237 this.fieldEditable = (fieldEditable == true);1238};1239/**1240 * Get field1241 * @return {String}1242 */1243Node.prototype.getField = function() {1244 if (this.field === undefined) {1245 this._getDomField();1246 }1247 return this.field;1248};1249/**1250 * Set value. Value is a JSON structure or an element String, Boolean, etc.1251 * @param {*} value1252 * @param {String} [type] Specify the type of the value. Can be 'auto',1253 * 'array', 'object', or 'string'1254 */1255Node.prototype.setValue = function(value, type) {1256 var childValue, child;1257 // first clear all current childs (if any)1258 var childs = this.childs;1259 if (childs) {1260 while (childs.length) {1261 this.removeChild(childs[0]);1262 }1263 }1264 // TODO: remove the DOM of this Node1265 this.type = this._getType(value);1266 // check if type corresponds with the provided type1267 if (type && type != this.type) {1268 if (type == 'string' && this.type == 'auto') {1269 this.type = type;1270 }1271 else {1272 throw new Error('Type mismatch: ' +1273 'cannot cast value of type "' + this.type +1274 ' to the specified type "' + type + '"');1275 }1276 }1277 if (this.type == 'array') {1278 // array1279 this.childs = [];1280 for (var i = 0, iMax = value.length; i < iMax; i++) {1281 childValue = value[i];1282 if (childValue !== undefined && !(childValue instanceof Function)) {1283 // ignore undefined and functions1284 child = new Node(this.editor, {1285 'value': childValue1286 });1287 this.appendChild(child);1288 }1289 }1290 this.value = '';1291 }1292 else if (this.type == 'object') {1293 // object1294 this.childs = [];1295 for (var childField in value) {1296 if (value.hasOwnProperty(childField)) {1297 childValue = value[childField];1298 if (childValue !== undefined && !(childValue instanceof Function)) {1299 // ignore undefined and functions1300 child = new Node(this.editor, {1301 'field': childField,1302 'value': childValue1303 });1304 this.appendChild(child);1305 }1306 }1307 }1308 this.value = '';1309 }1310 else {1311 // value1312 this.childs = undefined;1313 this.value = value;1314 /* TODO1315 if (typeof(value) == 'string') {1316 var escValue = JSON.stringify(value);1317 this.value = escValue.substring(1, escValue.length - 1);1318 util.log('check', value, this.value);1319 }1320 else {1321 this.value = value;1322 }1323 */1324 }1325};1326/**1327 * Get value. Value is a JSON structure1328 * @return {*} value1329 */1330Node.prototype.getValue = function() {1331 //var childs, i, iMax;1332 if (this.type == 'array') {1333 var arr = [];1334 this.childs.forEach (function (child) {1335 arr.push(child.getValue());1336 });1337 return arr;1338 }1339 else if (this.type == 'object') {1340 var obj = {};1341 this.childs.forEach (function (child) {1342 obj[child.getField()] = child.getValue();1343 });1344 return obj;1345 }1346 else {1347 if (this.value === undefined) {1348 this._getDomValue();1349 }1350 return this.value;1351 }1352};1353/**1354 * Get the nesting level of this node1355 * @return {Number} level1356 */1357Node.prototype.getLevel = function() {1358 return (this.parent ? this.parent.getLevel() + 1 : 0);1359};1360/**1361 * Create a clone of a node1362 * The complete state of a clone is copied, including whether it is expanded or1363 * not. The DOM elements are not cloned.1364 * @return {Node} clone1365 */1366Node.prototype.clone = function() {1367 var clone = new Node(this.editor);1368 clone.type = this.type;1369 clone.field = this.field;1370 clone.fieldInnerText = this.fieldInnerText;1371 clone.fieldEditable = this.fieldEditable;1372 clone.value = this.value;1373 clone.valueInnerText = this.valueInnerText;1374 clone.expanded = this.expanded;1375 if (this.childs) {1376 // an object or array1377 var cloneChilds = [];1378 this.childs.forEach(function (child) {1379 var childClone = child.clone();1380 childClone.setParent(clone);1381 cloneChilds.push(childClone);1382 });1383 clone.childs = cloneChilds;1384 }1385 else {1386 // a value1387 clone.childs = undefined;1388 }1389 return clone;1390};1391/**1392 * Expand this node and optionally its childs.1393 * @param {boolean} [recurse] Optional recursion, true by default. When1394 * true, all childs will be expanded recursively1395 */1396Node.prototype.expand = function(recurse) {1397 if (!this.childs) {1398 return;1399 }1400 // set this node expanded1401 this.expanded = true;1402 if (this.dom.expand) {1403 this.dom.expand.className = 'expanded';1404 }1405 this.showChilds();1406 if (recurse != false) {1407 this.childs.forEach(function (child) {1408 child.expand(recurse);1409 });1410 }1411};1412/**1413 * Collapse this node and optionally its childs.1414 * @param {boolean} [recurse] Optional recursion, true by default. When1415 * true, all childs will be collapsed recursively1416 */1417Node.prototype.collapse = function(recurse) {1418 if (!this.childs) {1419 return;1420 }1421 this.hideChilds();1422 // collapse childs in case of recurse1423 if (recurse != false) {1424 this.childs.forEach(function (child) {1425 child.collapse(recurse);1426 });1427 }1428 // make this node collapsed1429 if (this.dom.expand) {1430 this.dom.expand.className = 'collapsed';1431 }1432 this.expanded = false;1433};1434/**1435 * Recursively show all childs when they are expanded1436 */1437Node.prototype.showChilds = function() {1438 var childs = this.childs;1439 if (!childs) {1440 return;1441 }1442 if (!this.expanded) {1443 return;1444 }1445 var tr = this.dom.tr;1446 var table = tr ? tr.parentNode : undefined;1447 if (table) {1448 // show row with append button1449 var append = this.getAppend();1450 var nextTr = tr.nextSibling;1451 if (nextTr) {1452 table.insertBefore(append, nextTr);1453 }1454 else {1455 table.appendChild(append);1456 }1457 // show childs1458 this.childs.forEach(function (child) {1459 table.insertBefore(child.getDom(), append);1460 child.showChilds();1461 });1462 }1463};1464/**1465 * Hide the node with all its childs1466 */1467Node.prototype.hide = function() {1468 var tr = this.dom.tr;1469 var table = tr ? tr.parentNode : undefined;1470 if (table) {1471 table.removeChild(tr);1472 }1473 this.hideChilds();1474};1475/**1476 * Recursively hide all childs1477 */1478Node.prototype.hideChilds = function() {1479 var childs = this.childs;1480 if (!childs) {1481 return;1482 }1483 if (!this.expanded) {1484 return;1485 }1486 // hide append row1487 var append = this.getAppend();1488 if (append.parentNode) {1489 append.parentNode.removeChild(append);1490 }1491 // hide childs1492 this.childs.forEach(function (child) {1493 child.hide();1494 });1495};1496/**1497 * Add a new child to the node.1498 * Only applicable when Node value is of type array or object1499 * @param {Node} node1500 */1501Node.prototype.appendChild = function(node) {1502 if (this._hasChilds()) {1503 // adjust the link to the parent1504 node.setParent(this);1505 node.fieldEditable = (this.type == 'object');1506 if (this.type == 'array') {1507 node.index = this.childs.length;1508 }1509 this.childs.push(node);1510 if (this.expanded) {1511 // insert into the DOM, before the appendRow1512 var newTr = node.getDom();1513 var appendTr = this.getAppend();1514 var table = appendTr ? appendTr.parentNode : undefined;1515 if (appendTr && table) {1516 table.insertBefore(newTr, appendTr);1517 }1518 node.showChilds();1519 }1520 this.updateDom({'updateIndexes': true});1521 node.updateDom({'recurse': true});1522 }1523};1524/**1525 * Move a node from its current parent to this node1526 * Only applicable when Node value is of type array or object1527 * @param {Node} node1528 * @param {Node} beforeNode1529 */1530Node.prototype.moveBefore = function(node, beforeNode) {1531 if (this._hasChilds()) {1532 // create a temporary row, to prevent the scroll position from jumping1533 // when removing the node1534 var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;1535 if (tbody) {1536 var trTemp = document.createElement('tr');1537 trTemp.style.height = tbody.clientHeight + 'px';1538 tbody.appendChild(trTemp);1539 }1540 if (node.parent) {1541 node.parent.removeChild(node);1542 }1543 if (beforeNode instanceof AppendNode) {1544 this.appendChild(node);1545 }1546 else {1547 this.insertBefore(node, beforeNode);1548 }1549 if (tbody) {1550 tbody.removeChild(trTemp);1551 }1552 }1553};1554/**1555 * Move a node from its current parent to this node1556 * Only applicable when Node value is of type array or object.1557 * If index is out of range, the node will be appended to the end1558 * @param {Node} node1559 * @param {Number} index1560 */1561Node.prototype.moveTo = function (node, index) {1562 if (node.parent == this) {1563 // same parent1564 var currentIndex = this.childs.indexOf(node);1565 if (currentIndex < index) {1566 // compensate the index for removal of the node itself1567 index++;1568 }1569 }1570 var beforeNode = this.childs[index] || this.append;1571 this.moveBefore(node, beforeNode);1572};1573/**1574 * Insert a new child before a given node1575 * Only applicable when Node value is of type array or object1576 * @param {Node} node1577 * @param {Node} beforeNode1578 */1579Node.prototype.insertBefore = function(node, beforeNode) {1580 if (this._hasChilds()) {1581 if (beforeNode == this.append) {1582 // append to the child nodes1583 // adjust the link to the parent1584 node.setParent(this);1585 node.fieldEditable = (this.type == 'object');1586 this.childs.push(node);1587 }1588 else {1589 // insert before a child node1590 var index = this.childs.indexOf(beforeNode);1591 if (index == -1) {1592 throw new Error('Node not found');1593 }1594 // adjust the link to the parent1595 node.setParent(this);1596 node.fieldEditable = (this.type == 'object');1597 this.childs.splice(index, 0, node);1598 }1599 if (this.expanded) {1600 // insert into the DOM1601 var newTr = node.getDom();1602 var nextTr = beforeNode.getDom();1603 var table = nextTr ? nextTr.parentNode : undefined;1604 if (nextTr && table) {1605 table.insertBefore(newTr, nextTr);1606 }1607 node.showChilds();1608 }1609 this.updateDom({'updateIndexes': true});1610 node.updateDom({'recurse': true});1611 }1612};1613/**1614 * Insert a new child before a given node1615 * Only applicable when Node value is of type array or object1616 * @param {Node} node1617 * @param {Node} afterNode1618 */1619Node.prototype.insertAfter = function(node, afterNode) {1620 if (this._hasChilds()) {1621 var index = this.childs.indexOf(afterNode);1622 var beforeNode = this.childs[index + 1];1623 if (beforeNode) {1624 this.insertBefore(node, beforeNode);1625 }1626 else {1627 this.appendChild(node);1628 }1629 }1630};1631/**1632 * Search in this node1633 * The node will be expanded when the text is found one of its childs, else1634 * it will be collapsed. Searches are case insensitive.1635 * @param {String} text1636 * @return {Node[]} results Array with nodes containing the search text1637 */1638Node.prototype.search = function(text) {1639 var results = [];1640 var index;1641 var search = text ? text.toLowerCase() : undefined;1642 // delete old search data1643 delete this.searchField;1644 delete this.searchValue;1645 // search in field1646 if (this.field != undefined) {1647 var field = String(this.field).toLowerCase();1648 index = field.indexOf(search);1649 if (index != -1) {1650 this.searchField = true;1651 results.push({1652 'node': this,1653 'elem': 'field'1654 });1655 }1656 // update dom1657 this._updateDomField();1658 }1659 // search in value1660 if (this._hasChilds()) {1661 // array, object1662 // search the nodes childs1663 if (this.childs) {1664 var childResults = [];1665 this.childs.forEach(function (child) {1666 childResults = childResults.concat(child.search(text));1667 });1668 results = results.concat(childResults);1669 }1670 // update dom1671 if (search != undefined) {1672 var recurse = false;1673 if (childResults.length == 0) {1674 this.collapse(recurse);1675 }1676 else {1677 this.expand(recurse);1678 }1679 }1680 }1681 else {1682 // string, auto1683 if (this.value != undefined ) {1684 var value = String(this.value).toLowerCase();1685 index = value.indexOf(search);1686 if (index != -1) {1687 this.searchValue = true;1688 results.push({1689 'node': this,1690 'elem': 'value'1691 });1692 }1693 }1694 // update dom1695 this._updateDomValue();1696 }1697 return results;1698};1699/**1700 * Move the scroll position such that this node is in the visible area.1701 * The node will not get the focus1702 * @param {function(boolean)} [callback]1703 */1704Node.prototype.scrollTo = function(callback) {1705 if (!this.dom.tr || !this.dom.tr.parentNode) {1706 // if the node is not visible, expand its parents1707 var parent = this.parent;1708 var recurse = false;1709 while (parent) {1710 parent.expand(recurse);1711 parent = parent.parent;1712 }1713 }1714 if (this.dom.tr && this.dom.tr.parentNode) {1715 this.editor.scrollTo(this.dom.tr.offsetTop, callback);1716 }1717};1718// stores the element name currently having the focus1719Node.focusElement = undefined;1720/**1721 * Set focus to this node1722 * @param {String} [elementName] The field name of the element to get the1723 * focus available values: 'drag', 'menu',1724 * 'expand', 'field', 'value' (default)1725 */1726Node.prototype.focus = function(elementName) {1727 Node.focusElement = elementName;1728 if (this.dom.tr && this.dom.tr.parentNode) {1729 var dom = this.dom;1730 switch (elementName) {1731 case 'drag':1732 if (dom.drag) {1733 dom.drag.focus();1734 }1735 else {1736 dom.menu.focus();1737 }1738 break;1739 case 'menu':1740 dom.menu.focus();1741 break;1742 case 'expand':1743 if (this._hasChilds()) {1744 dom.expand.focus();1745 }1746 else if (dom.field && this.fieldEditable) {1747 dom.field.focus();1748 util.selectContentEditable(dom.field);1749 }1750 else if (dom.value && !this._hasChilds()) {1751 dom.value.focus();1752 util.selectContentEditable(dom.value);1753 }1754 else {1755 dom.menu.focus();1756 }1757 break;1758 case 'field':1759 if (dom.field && this.fieldEditable) {1760 dom.field.focus();1761 util.selectContentEditable(dom.field);1762 }1763 else if (dom.value && !this._hasChilds()) {1764 dom.value.focus();1765 util.selectContentEditable(dom.value);1766 }1767 else if (this._hasChilds()) {1768 dom.expand.focus();1769 }1770 else {1771 dom.menu.focus();1772 }1773 break;1774 case 'value':1775 default:1776 if (dom.value && !this._hasChilds()) {1777 dom.value.focus();1778 util.selectContentEditable(dom.value);1779 }1780 else if (dom.field && this.fieldEditable) {1781 dom.field.focus();1782 util.selectContentEditable(dom.field);1783 }1784 else if (this._hasChilds()) {1785 dom.expand.focus();1786 }1787 else {1788 dom.menu.focus();1789 }1790 break;1791 }1792 }1793};1794/**1795 * Select all text in an editable div after a delay of 0 ms1796 * @param {Element} editableDiv1797 */1798Node.select = function(editableDiv) {1799 setTimeout(function () {1800 util.selectContentEditable(editableDiv);1801 }, 0);1802};1803/**1804 * Update the values from the DOM field and value of this node1805 */1806Node.prototype.blur = function() {1807 // retrieve the actual field and value from the DOM.1808 this._getDomValue(false);1809 this._getDomField(false);1810};1811/**1812 * Duplicate given child node1813 * new structure will be added right before the cloned node1814 * @param {Node} node the childNode to be duplicated1815 * @return {Node} clone the clone of the node1816 * @private1817 */1818Node.prototype._duplicate = function(node) {1819 var clone = node.clone();1820 /* TODO: adjust the field name (to prevent equal field names)1821 if (this.type == 'object') {1822 }1823 */1824 this.insertAfter(clone, node);1825 return clone;1826};1827/**1828 * Check if given node is a child. The method will check recursively to find1829 * this node.1830 * @param {Node} node1831 * @return {boolean} containsNode1832 */1833Node.prototype.containsNode = function(node) {1834 if (this == node) {1835 return true;1836 }1837 var childs = this.childs;1838 if (childs) {1839 // TODO: use the js5 Array.some() here?1840 for (var i = 0, iMax = childs.length; i < iMax; i++) {1841 if (childs[i].containsNode(node)) {1842 return true;1843 }1844 }1845 }1846 return false;1847};1848/**1849 * Move given node into this node1850 * @param {Node} node the childNode to be moved1851 * @param {Node} beforeNode node will be inserted before given1852 * node. If no beforeNode is given,1853 * the node is appended at the end1854 * @private1855 */1856Node.prototype._move = function(node, beforeNode) {1857 if (node == beforeNode) {1858 // nothing to do...1859 return;1860 }1861 // check if this node is not a child of the node to be moved here1862 if (node.containsNode(this)) {1863 throw new Error('Cannot move a field into a child of itself');1864 }1865 // remove the original node1866 if (node.parent) {1867 node.parent.removeChild(node);1868 }1869 // create a clone of the node1870 var clone = node.clone();1871 node.clearDom();1872 // insert or append the node1873 if (beforeNode) {1874 this.insertBefore(clone, beforeNode);1875 }1876 else {1877 this.appendChild(clone);1878 }1879 /* TODO: adjust the field name (to prevent equal field names)1880 if (this.type == 'object') {1881 }1882 */1883};1884/**1885 * Remove a child from the node.1886 * Only applicable when Node value is of type array or object1887 * @param {Node} node The child node to be removed;1888 * @return {Node | undefined} node The removed node on success,1889 * else undefined1890 */1891Node.prototype.removeChild = function(node) {1892 if (this.childs) {1893 var index = this.childs.indexOf(node);1894 if (index != -1) {1895 node.hide();1896 // delete old search results1897 delete node.searchField;1898 delete node.searchValue;1899 var removedNode = this.childs.splice(index, 1)[0];1900 this.updateDom({'updateIndexes': true});1901 return removedNode;1902 }1903 }1904 return undefined;1905};1906/**1907 * Remove a child node node from this node1908 * This method is equal to Node.removeChild, except that _remove firex an1909 * onChange event.1910 * @param {Node} node1911 * @private1912 */1913Node.prototype._remove = function (node) {1914 this.removeChild(node);1915};1916/**1917 * Change the type of the value of this Node1918 * @param {String} newType1919 */1920Node.prototype.changeType = function (newType) {1921 var oldType = this.type;1922 if (oldType == newType) {1923 // type is not changed1924 return;1925 }1926 if ((newType == 'string' || newType == 'auto') &&1927 (oldType == 'string' || oldType == 'auto')) {1928 // this is an easy change1929 this.type = newType;1930 }1931 else {1932 // change from array to object, or from string/auto to object/array1933 var table = this.dom.tr ? this.dom.tr.parentNode : undefined;1934 var lastTr;1935 if (this.expanded) {1936 lastTr = this.getAppend();1937 }1938 else {1939 lastTr = this.getDom();1940 }1941 var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;1942 // hide current field and all its childs1943 this.hide();1944 this.clearDom();1945 // adjust the field and the value1946 this.type = newType;1947 // adjust childs1948 if (newType == 'object') {1949 if (!this.childs) {1950 this.childs = [];1951 }1952 this.childs.forEach(function (child, index) {1953 child.clearDom();1954 delete child.index;1955 child.fieldEditable = true;1956 if (child.field == undefined) {1957 child.field = '';1958 }1959 });1960 if (oldType == 'string' || oldType == 'auto') {1961 this.expanded = true;1962 }1963 }1964 else if (newType == 'array') {1965 if (!this.childs) {1966 this.childs = [];1967 }1968 this.childs.forEach(function (child, index) {1969 child.clearDom();1970 child.fieldEditable = false;1971 child.index = index;1972 });1973 if (oldType == 'string' || oldType == 'auto') {1974 this.expanded = true;1975 }1976 }1977 else {1978 this.expanded = false;1979 }1980 // create new DOM1981 if (table) {1982 if (nextTr) {1983 table.insertBefore(this.getDom(), nextTr);1984 }1985 else {1986 table.appendChild(this.getDom());1987 }1988 }1989 this.showChilds();1990 }1991 if (newType == 'auto' || newType == 'string') {1992 // cast value to the correct type1993 if (newType == 'string') {1994 this.value = String(this.value);1995 }1996 else {1997 this.value = this._stringCast(String(this.value));1998 }1999 this.focus();2000 }2001 this.updateDom({'updateIndexes': true});2002};2003/**2004 * Retrieve value from DOM2005 * @param {boolean} [silent] If true (default), no errors will be thrown in2006 * case of invalid data2007 * @private2008 */2009Node.prototype._getDomValue = function(silent) {2010 if (this.dom.value && this.type != 'array' && this.type != 'object') {2011 this.valueInnerText = util.getInnerText(this.dom.value);2012 }2013 if (this.valueInnerText != undefined) {2014 try {2015 // retrieve the value2016 var value;2017 if (this.type == 'string') {2018 value = this._unescapeHTML(this.valueInnerText);2019 }2020 else {2021 var str = this._unescapeHTML(this.valueInnerText);2022 value = this._stringCast(str);2023 }2024 if (value !== this.value) {2025 var oldValue = this.value;2026 this.value = value;2027 this.editor._onAction('editValue', {2028 'node': this,2029 'oldValue': oldValue,2030 'newValue': value,2031 'oldSelection': this.editor.selection,2032 'newSelection': this.editor.getSelection()2033 });2034 }2035 }2036 catch (err) {2037 this.value = undefined;2038 // TODO: sent an action with the new, invalid value?2039 if (silent != true) {2040 throw err;2041 }2042 }2043 }2044};2045/**2046 * Update dom value:2047 * - the text color of the value, depending on the type of the value2048 * - the height of the field, depending on the width2049 * - background color in case it is empty2050 * @private2051 */2052Node.prototype._updateDomValue = function () {2053 var domValue = this.dom.value;2054 if (domValue) {2055 // set text color depending on value type2056 // TODO: put colors in css2057 var v = this.value;2058 var t = (this.type == 'auto') ? util.type(v) : this.type;2059 var isUrl = (t == 'string' && util.isUrl(v));2060 var color = '';2061 if (isUrl && !this.editor.mode.edit) {2062 color = '';2063 }2064 else if (t == 'string') {2065 color = 'green';2066 }2067 else if (t == 'number') {2068 color = 'red';2069 }2070 else if (t == 'boolean') {2071 color = 'darkorange';2072 }2073 else if (this._hasChilds()) {2074 color = '';2075 }2076 else if (v === null) {2077 color = '#004ED0'; // blue2078 }2079 else {2080 // invalid value2081 color = 'black';2082 }2083 domValue.style.color = color;2084 // make background color light-gray when empty2085 var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');2086 if (isEmpty) {2087 util.addClassName(domValue, 'empty');2088 }2089 else {2090 util.removeClassName(domValue, 'empty');2091 }2092 // underline url2093 if (isUrl) {2094 util.addClassName(domValue, 'url');2095 }2096 else {2097 util.removeClassName(domValue, 'url');2098 }2099 // update title2100 if (t == 'array' || t == 'object') {2101 var count = this.childs ? this.childs.length : 0;2102 domValue.title = this.type + ' containing ' + count + ' items';2103 }2104 else if (t == 'string' && util.isUrl(v)) {2105 if (this.editor.mode.edit) {2106 domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';2107 }2108 }2109 else {2110 domValue.title = '';2111 }2112 // highlight when there is a search result2113 if (this.searchValueActive) {2114 util.addClassName(domValue, 'highlight-active');2115 }2116 else {2117 util.removeClassName(domValue, 'highlight-active');2118 }2119 if (this.searchValue) {2120 util.addClassName(domValue, 'highlight');2121 }2122 else {2123 util.removeClassName(domValue, 'highlight');2124 }2125 // strip formatting from the contents of the editable div2126 util.stripFormatting(domValue);2127 }2128};2129/**2130 * Update dom field:2131 * - the text color of the field, depending on the text2132 * - the height of the field, depending on the width2133 * - background color in case it is empty2134 * @private2135 */2136Node.prototype._updateDomField = function () {2137 var domField = this.dom.field;2138 if (domField) {2139 // make backgound color lightgray when empty2140 var isEmpty = (String(this.field) == '' && this.parent.type != 'array');2141 if (isEmpty) {2142 util.addClassName(domField, 'empty');2143 }2144 else {2145 util.removeClassName(domField, 'empty');2146 }2147 // highlight when there is a search result2148 if (this.searchFieldActive) {2149 util.addClassName(domField, 'highlight-active');2150 }2151 else {2152 util.removeClassName(domField, 'highlight-active');2153 }2154 if (this.searchField) {2155 util.addClassName(domField, 'highlight');2156 }2157 else {2158 util.removeClassName(domField, 'highlight');2159 }2160 // strip formatting from the contents of the editable div2161 util.stripFormatting(domField);2162 }2163};2164/**2165 * Retrieve field from DOM2166 * @param {boolean} [silent] If true (default), no errors will be thrown in2167 * case of invalid data2168 * @private2169 */2170Node.prototype._getDomField = function(silent) {2171 if (this.dom.field && this.fieldEditable) {2172 this.fieldInnerText = util.getInnerText(this.dom.field);2173 }2174 if (this.fieldInnerText != undefined) {2175 try {2176 var field = this._unescapeHTML(this.fieldInnerText);2177 if (field !== this.field) {2178 var oldField = this.field;2179 this.field = field;2180 this.editor._onAction('editField', {2181 'node': this,2182 'oldValue': oldField,2183 'newValue': field,2184 'oldSelection': this.editor.selection,2185 'newSelection': this.editor.getSelection()2186 });2187 }2188 }2189 catch (err) {2190 this.field = undefined;2191 // TODO: sent an action here, with the new, invalid value?2192 if (silent != true) {2193 throw err;2194 }2195 }2196 }2197};2198/**2199 * Clear the dom of the node2200 */2201Node.prototype.clearDom = function() {2202 // TODO: hide the node first?2203 //this.hide();2204 // TODO: recursively clear dom?2205 this.dom = {};2206};2207/**2208 * Get the HTML DOM TR element of the node.2209 * The dom will be generated when not yet created2210 * @return {Element} tr HTML DOM TR Element2211 */2212Node.prototype.getDom = function() {2213 var dom = this.dom;2214 if (dom.tr) {2215 return dom.tr;2216 }2217 // create row2218 dom.tr = document.createElement('tr');2219 dom.tr.node = this;2220 if (this.editor.mode.edit) {2221 // create draggable area2222 var tdDrag = document.createElement('td');2223 if (this.parent) {2224 var domDrag = document.createElement('button');2225 dom.drag = domDrag;2226 domDrag.className = 'dragarea';2227 domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';2228 tdDrag.appendChild(domDrag);2229 }2230 dom.tr.appendChild(tdDrag);2231 // create context menu2232 var tdMenu = document.createElement('td');2233 var menu = document.createElement('button');2234 dom.menu = menu;2235 menu.className = 'contextmenu';2236 menu.title = 'Click to open the actions menu (Ctrl+M)';2237 tdMenu.appendChild(dom.menu);2238 dom.tr.appendChild(tdMenu);2239 }2240 // create tree and field2241 var tdField = document.createElement('td');2242 dom.tr.appendChild(tdField);2243 dom.tree = this._createDomTree();2244 tdField.appendChild(dom.tree);2245 this.updateDom({'updateIndexes': true});2246 return dom.tr;2247};2248/**2249 * DragStart event, fired on mousedown on the dragarea at the left side of a Node2250 * @param {Event} event2251 * @private2252 */2253Node.prototype._onDragStart = function (event) {2254 event = event || window.event;2255 var node = this;2256 if (!this.mousemove) {2257 this.mousemove = util.addEventListener(document, 'mousemove',2258 function (event) {2259 node._onDrag(event);2260 });2261 }2262 if (!this.mouseup) {2263 this.mouseup = util.addEventListener(document, 'mouseup',2264 function (event ) {2265 node._onDragEnd(event);2266 });2267 }2268 this.editor.highlighter.lock();2269 this.drag = {2270 'oldCursor': document.body.style.cursor,2271 'startParent': this.parent,2272 'startIndex': this.parent.childs.indexOf(this),2273 'mouseX': util.getMouseX(event),2274 'level': this.getLevel()2275 };2276 document.body.style.cursor = 'move';2277 util.preventDefault(event);2278};2279/**2280 * Drag event, fired when moving the mouse while dragging a Node2281 * @param {Event} event2282 * @private2283 */2284Node.prototype._onDrag = function (event) {2285 // TODO: this method has grown too large. Split it in a number of methods2286 event = event || window.event;2287 var mouseY = util.getMouseY(event);2288 var mouseX = util.getMouseX(event);2289 var trThis, trPrev, trNext, trFirst, trLast, trRoot;2290 var nodePrev, nodeNext;2291 var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;2292 var moved = false;2293 // TODO: add an ESC option, which resets to the original position2294 // move up/down2295 trThis = this.dom.tr;2296 topThis = util.getAbsoluteTop(trThis);2297 heightThis = trThis.offsetHeight;2298 if (mouseY < topThis) {2299 // move up2300 trPrev = trThis;2301 do {2302 trPrev = trPrev.previousSibling;2303 nodePrev = Node.getNodeFromTarget(trPrev);2304 topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;2305 }2306 while (trPrev && mouseY < topPrev);2307 if (nodePrev && !nodePrev.parent) {2308 nodePrev = undefined;2309 }2310 if (!nodePrev) {2311 // move to the first node2312 trRoot = trThis.parentNode.firstChild;2313 trPrev = trRoot ? trRoot.nextSibling : undefined;2314 nodePrev = Node.getNodeFromTarget(trPrev);2315 if (nodePrev == this) {2316 nodePrev = undefined;2317 }2318 }2319 if (nodePrev) {2320 // check if mouseY is really inside the found node2321 trPrev = nodePrev.dom.tr;2322 topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;2323 if (mouseY > topPrev + heightThis) {2324 nodePrev = undefined;2325 }2326 }2327 if (nodePrev) {2328 nodePrev.parent.moveBefore(this, nodePrev);2329 moved = true;2330 }2331 }2332 else {2333 // move down2334 trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;2335 trFirst = trLast ? trLast.nextSibling : undefined;2336 if (trFirst) {2337 topFirst = util.getAbsoluteTop(trFirst);2338 trNext = trFirst;2339 do {2340 nodeNext = Node.getNodeFromTarget(trNext);2341 if (trNext) {2342 bottomNext = trNext.nextSibling ?2343 util.getAbsoluteTop(trNext.nextSibling) : 0;2344 heightNext = trNext ? (bottomNext - topFirst) : 0;2345 if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {2346 // We are about to remove the last child of this parent,2347 // which will make the parents appendNode visible.2348 topThis += 24 - 1;2349 // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.2350 }2351 }2352 trNext = trNext.nextSibling;2353 }2354 while (trNext && mouseY > topThis + heightNext);2355 if (nodeNext && nodeNext.parent) {2356 // calculate the desired level2357 var diffX = (mouseX - this.drag.mouseX);2358 var diffLevel = Math.round(diffX / 24 / 2);2359 var level = this.drag.level + diffLevel; // desired level2360 var levelNext = nodeNext.getLevel(); // level to be2361 // find the best fitting level (move upwards over the append nodes)2362 trPrev = nodeNext.dom.tr.previousSibling;2363 while (levelNext < level && trPrev) {2364 nodePrev = Node.getNodeFromTarget(trPrev);2365 if (nodePrev == this || nodePrev._isChildOf(this)) {2366 // neglect itself and its childs2367 }2368 else if (nodePrev instanceof AppendNode) {2369 var childs = nodePrev.parent.childs;2370 if (childs.length > 1 ||2371 (childs.length == 1 && childs[0] != this)) {2372 // non-visible append node of a list of childs2373 // consisting of not only this node (else the2374 // append node will change into a visible "empty"2375 // text when removing this node).2376 nodeNext = Node.getNodeFromTarget(trPrev);2377 levelNext = nodeNext.getLevel();2378 }2379 else {2380 break;2381 }2382 }2383 else {2384 break;2385 }2386 trPrev = trPrev.previousSibling;2387 }2388 // move the node when its position is changed2389 if (trLast.nextSibling != nodeNext.dom.tr) {2390 nodeNext.parent.moveBefore(this, nodeNext);2391 moved = true;2392 }2393 }2394 }2395 }2396 if (moved) {2397 // update the dragging parameters when moved2398 this.drag.mouseX = mouseX;2399 this.drag.level = this.getLevel();2400 }2401 // auto scroll when hovering around the top of the editor2402 this.editor.startAutoScroll(mouseY);2403 util.preventDefault(event);2404};2405/**2406 * Drag event, fired on mouseup after having dragged a node2407 * @param {Event} event2408 * @private2409 */2410Node.prototype._onDragEnd = function (event) {2411 event = event || window.event;2412 var params = {2413 'node': this,2414 'startParent': this.drag.startParent,2415 'startIndex': this.drag.startIndex,2416 'endParent': this.parent,2417 'endIndex': this.parent.childs.indexOf(this)2418 };2419 if ((params.startParent != params.endParent) ||2420 (params.startIndex != params.endIndex)) {2421 // only register this action if the node is actually moved to another place2422 this.editor._onAction('moveNode', params);2423 }2424 document.body.style.cursor = this.drag.oldCursor;2425 this.editor.highlighter.unlock();2426 delete this.drag;2427 if (this.mousemove) {2428 util.removeEventListener(document, 'mousemove', this.mousemove);2429 delete this.mousemove;}2430 if (this.mouseup) {2431 util.removeEventListener(document, 'mouseup', this.mouseup);2432 delete this.mouseup;2433 }2434 // Stop any running auto scroll2435 this.editor.stopAutoScroll();2436 util.preventDefault(event);2437};2438/**2439 * Test if this node is a child of an other node2440 * @param {Node} node2441 * @return {boolean} isChild2442 * @private2443 */2444Node.prototype._isChildOf = function (node) {2445 var n = this.parent;2446 while (n) {2447 if (n == node) {2448 return true;2449 }2450 n = n.parent;2451 }2452 return false;2453};2454/**2455 * Create an editable field2456 * @return {Element} domField2457 * @private2458 */2459Node.prototype._createDomField = function () {2460 return document.createElement('div');2461};2462/**2463 * Set highlighting for this node and all its childs.2464 * Only applied to the currently visible (expanded childs)2465 * @param {boolean} highlight2466 */2467Node.prototype.setHighlight = function (highlight) {2468 if (this.dom.tr) {2469 this.dom.tr.className = (highlight ? 'highlight' : '');2470 if (this.append) {2471 this.append.setHighlight(highlight);2472 }2473 if (this.childs) {2474 this.childs.forEach(function (child) {2475 child.setHighlight(highlight);2476 });2477 }2478 }2479};2480/**2481 * Update the value of the node. Only primitive types are allowed, no Object2482 * or Array is allowed.2483 * @param {String | Number | Boolean | null} value2484 */2485Node.prototype.updateValue = function (value) {2486 this.value = value;2487 this.updateDom();2488};2489/**2490 * Update the field of the node.2491 * @param {String} field2492 */2493Node.prototype.updateField = function (field) {2494 this.field = field;2495 this.updateDom();2496};2497/**2498 * Update the HTML DOM, optionally recursing through the childs2499 * @param {Object} [options] Available parameters:2500 * {boolean} [recurse] If true, the2501 * DOM of the childs will be updated recursively.2502 * False by default.2503 * {boolean} [updateIndexes] If true, the childs2504 * indexes of the node will be updated too. False by2505 * default.2506 */2507Node.prototype.updateDom = function (options) {2508 // update level indentation2509 var domTree = this.dom.tree;2510 if (domTree) {2511 domTree.style.marginLeft = this.getLevel() * 24 + 'px';2512 }2513 // update field2514 var domField = this.dom.field;2515 if (domField) {2516 if (this.fieldEditable == true) {2517 // parent is an object2518 domField.contentEditable = this.editor.mode.edit;2519 domField.spellcheck = false;2520 domField.className = 'field';2521 }2522 else {2523 // parent is an array this is the root node2524 domField.className = 'readonly';2525 }2526 var field;2527 if (this.index != undefined) {2528 field = this.index;2529 }2530 else if (this.field != undefined) {2531 field = this.field;2532 }2533 else if (this._hasChilds()) {2534 field = this.type;2535 }2536 else {2537 field = '';2538 }2539 domField.innerHTML = this._escapeHTML(field);2540 }2541 // update value2542 var domValue = this.dom.value;2543 if (domValue) {2544 var count = this.childs ? this.childs.length : 0;2545 if (this.type == 'array') {2546 domValue.innerHTML = '[' + count + ']';2547 }2548 else if (this.type == 'object') {2549 domValue.innerHTML = '{' + count + '}';2550 }2551 else {2552 domValue.innerHTML = this._escapeHTML(this.value);2553 }2554 }2555 // update field and value2556 this._updateDomField();2557 this._updateDomValue();2558 // update childs indexes2559 if (options && options.updateIndexes == true) {2560 // updateIndexes is true or undefined2561 this._updateDomIndexes();2562 }2563 if (options && options.recurse == true) {2564 // recurse is true or undefined. update childs recursively2565 if (this.childs) {2566 this.childs.forEach(function (child) {2567 child.updateDom(options);2568 });2569 }2570 }2571 // update row with append button2572 if (this.append) {2573 this.append.updateDom();2574 }2575};2576/**2577 * Update the DOM of the childs of a node: update indexes and undefined field2578 * names.2579 * Only applicable when structure is an array or object2580 * @private2581 */2582Node.prototype._updateDomIndexes = function () {2583 var domValue = this.dom.value;2584 var childs = this.childs;2585 if (domValue && childs) {2586 if (this.type == 'array') {2587 childs.forEach(function (child, index) {2588 child.index = index;2589 var childField = child.dom.field;2590 if (childField) {2591 childField.innerHTML = index;2592 }2593 });2594 }2595 else if (this.type == 'object') {2596 childs.forEach(function (child) {2597 if (child.index != undefined) {2598 delete child.index;2599 if (child.field == undefined) {2600 child.field = '';2601 }2602 }2603 });2604 }2605 }2606};2607/**2608 * Create an editable value2609 * @private2610 */2611Node.prototype._createDomValue = function () {2612 var domValue;2613 if (this.type == 'array') {2614 domValue = document.createElement('div');2615 domValue.className = 'readonly';2616 domValue.innerHTML = '[...]';2617 }2618 else if (this.type == 'object') {2619 domValue = document.createElement('div');2620 domValue.className = 'readonly';2621 domValue.innerHTML = '{...}';2622 }2623 else {2624 if (!this.editor.mode.edit && util.isUrl(this.value)) {2625 // create a link in case of read-only editor and value containing an url2626 domValue = document.createElement('a');2627 domValue.className = 'value';2628 domValue.href = this.value;2629 domValue.target = '_blank';2630 domValue.innerHTML = this._escapeHTML(this.value);2631 }2632 else {2633 // create and editable or read-only div2634 domValue = document.createElement('div');2635 domValue.contentEditable = !this.editor.mode.view;2636 domValue.spellcheck = false;2637 domValue.className = 'value';2638 domValue.innerHTML = this._escapeHTML(this.value);2639 }2640 }2641 return domValue;2642};2643/**2644 * Create an expand/collapse button2645 * @return {Element} expand2646 * @private2647 */2648Node.prototype._createDomExpandButton = function () {2649 // create expand button2650 var expand = document.createElement('button');2651 if (this._hasChilds()) {2652 expand.className = this.expanded ? 'expanded' : 'collapsed';2653 expand.title =2654 'Click to expand/collapse this field (Ctrl+E). \n' +2655 'Ctrl+Click to expand/collapse including all childs.';2656 }2657 else {2658 expand.className = 'invisible';2659 expand.title = '';2660 }2661 return expand;2662};2663/**2664 * Create a DOM tree element, containing the expand/collapse button2665 * @return {Element} domTree2666 * @private2667 */2668Node.prototype._createDomTree = function () {2669 var dom = this.dom;2670 var domTree = document.createElement('table');2671 var tbody = document.createElement('tbody');2672 domTree.style.borderCollapse = 'collapse'; // TODO: put in css2673 domTree.className = 'values';2674 domTree.appendChild(tbody);2675 var tr = document.createElement('tr');2676 tbody.appendChild(tr);2677 // create expand button2678 var tdExpand = document.createElement('td');2679 tdExpand.className = 'tree';2680 tr.appendChild(tdExpand);2681 dom.expand = this._createDomExpandButton();2682 tdExpand.appendChild(dom.expand);2683 dom.tdExpand = tdExpand;2684 // create the field2685 var tdField = document.createElement('td');2686 tdField.className = 'tree';2687 tr.appendChild(tdField);2688 dom.field = this._createDomField();2689 tdField.appendChild(dom.field);2690 dom.tdField = tdField;2691 // create a separator2692 var tdSeparator = document.createElement('td');2693 tdSeparator.className = 'tree';2694 tr.appendChild(tdSeparator);2695 if (this.type != 'object' && this.type != 'array') {2696 tdSeparator.appendChild(document.createTextNode(':'));2697 tdSeparator.className = 'separator';2698 }2699 dom.tdSeparator = tdSeparator;2700 // create the value2701 var tdValue = document.createElement('td');2702 tdValue.className = 'tree';2703 tr.appendChild(tdValue);2704 dom.value = this._createDomValue();2705 tdValue.appendChild(dom.value);2706 dom.tdValue = tdValue;2707 return domTree;2708};2709/**2710 * Handle an event. The event is catched centrally by the editor2711 * @param {Event} event2712 */2713Node.prototype.onEvent = function (event) {2714 var type = event.type,2715 target = event.target || event.srcElement,2716 dom = this.dom,2717 node = this,2718 focusNode,2719 expandable = this._hasChilds();2720 // check if mouse is on menu or on dragarea.2721 // If so, highlight current row and its childs2722 if (target == dom.drag || target == dom.menu) {2723 if (type == 'mouseover') {2724 this.editor.highlighter.highlight(this);2725 }2726 else if (type == 'mouseout') {2727 this.editor.highlighter.unhighlight();2728 }2729 }2730 // drag events2731 if (type == 'mousedown' && target == dom.drag) {2732 this._onDragStart(event);2733 }2734 // context menu events2735 if (type == 'click' && target == dom.menu) {2736 var highlighter = node.editor.highlighter;2737 highlighter.highlight(node);2738 highlighter.lock();2739 util.addClassName(dom.menu, 'selected');2740 this.showContextMenu(dom.menu, function () {2741 util.removeClassName(dom.menu, 'selected');2742 highlighter.unlock();2743 highlighter.unhighlight();2744 });2745 }2746 // expand events2747 if (type == 'click' && target == dom.expand) {2748 if (expandable) {2749 var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all2750 this._onExpand(recurse);2751 }2752 }2753 // value events2754 var domValue = dom.value;2755 if (target == domValue) {2756 //noinspection FallthroughInSwitchStatementJS2757 switch (type) {2758 case 'focus':2759 focusNode = this;2760 break;2761 case 'blur':2762 case 'change':2763 this._getDomValue(true);2764 this._updateDomValue();2765 if (this.value) {2766 domValue.innerHTML = this._escapeHTML(this.value);2767 }2768 break;2769 case 'input':2770 this._getDomValue(true);2771 this._updateDomValue();2772 break;2773 case 'keydown':2774 case 'mousedown':2775 this.editor.selection = this.editor.getSelection();2776 break;2777 case 'click':2778 if (event.ctrlKey && this.editor.mode.edit) {2779 if (util.isUrl(this.value)) {2780 window.open(this.value, '_blank');2781 }2782 }2783 break;2784 case 'keyup':2785 this._getDomValue(true);2786 this._updateDomValue();2787 break;2788 case 'cut':2789 case 'paste':2790 setTimeout(function () {2791 node._getDomValue(true);2792 node._updateDomValue();2793 }, 1);2794 break;2795 }2796 }2797 // field events2798 var domField = dom.field;2799 if (target == domField) {2800 switch (type) {2801 case 'focus':2802 focusNode = this;2803 break;2804 case 'blur':2805 case 'change':2806 this._getDomField(true);2807 this._updateDomField();2808 if (this.field) {2809 domField.innerHTML = this._escapeHTML(this.field);2810 }2811 break;2812 case 'input':2813 this._getDomField(true);2814 this._updateDomField();2815 break;2816 case 'keydown':2817 case 'mousedown':2818 this.editor.selection = this.editor.getSelection();2819 break;2820 case 'keyup':2821 this._getDomField(true);2822 this._updateDomField();2823 break;2824 case 'cut':2825 case 'paste':2826 setTimeout(function () {2827 node._getDomField(true);2828 node._updateDomField();2829 }, 1);2830 break;2831 }2832 }2833 // focus2834 // when clicked in whitespace left or right from the field or value, set focus2835 var domTree = dom.tree;2836 if (target == domTree.parentNode) {2837 switch (type) {2838 case 'click':2839 var left = (event.offsetX != undefined) ?2840 (event.offsetX < (this.getLevel() + 1) * 24) :2841 (util.getMouseX(event) < util.getAbsoluteLeft(dom.tdSeparator));// for FF2842 if (left || expandable) {2843 // node is expandable when it is an object or array2844 if (domField) {2845 util.setEndOfContentEditable(domField);2846 domField.focus();2847 }2848 }2849 else {2850 if (domValue) {2851 util.setEndOfContentEditable(domValue);2852 domValue.focus();2853 }2854 }2855 break;2856 }2857 }2858 if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||2859 target == dom.tdSeparator) {2860 switch (type) {2861 case 'click':2862 if (domField) {2863 util.setEndOfContentEditable(domField);2864 domField.focus();2865 }2866 break;2867 }2868 }2869 if (type == 'keydown') {2870 this.onKeyDown(event);2871 }2872};2873/**2874 * Key down event handler2875 * @param {Event} event2876 */2877Node.prototype.onKeyDown = function (event) {2878 var keynum = event.which || event.keyCode;2879 var target = event.target || event.srcElement;2880 var ctrlKey = event.ctrlKey;2881 var shiftKey = event.shiftKey;2882 var altKey = event.altKey;2883 var handled = false;2884 var prevNode, nextNode, nextDom, nextDom2;2885 // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup2886 if (keynum == 13) { // Enter2887 if (target == this.dom.value) {2888 if (!this.editor.mode.edit || event.ctrlKey) {2889 if (util.isUrl(this.value)) {2890 window.open(this.value, '_blank');2891 handled = true;2892 }2893 }2894 }2895 else if (target == this.dom.expand) {2896 var expandable = this._hasChilds();2897 if (expandable) {2898 var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all2899 this._onExpand(recurse);2900 target.focus();2901 handled = true;2902 }2903 }2904 }2905 else if (keynum == 68) { // D2906 if (ctrlKey) { // Ctrl+D2907 this._onDuplicate();2908 handled = true;2909 }2910 }2911 else if (keynum == 69) { // E2912 if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E2913 this._onExpand(shiftKey); // recurse = shiftKey2914 target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)2915 handled = true;2916 }2917 }2918 else if (keynum == 77) { // M2919 if (ctrlKey) { // Ctrl+M2920 this.showContextMenu(target);2921 handled = true;2922 }2923 }2924 else if (keynum == 46) { // Del2925 if (ctrlKey) { // Ctrl+Del2926 this._onRemove();2927 handled = true;2928 }2929 }2930 else if (keynum == 45) { // Ins2931 if (ctrlKey && !shiftKey) { // Ctrl+Ins2932 this._onInsertBefore();2933 handled = true;2934 }2935 else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins2936 this._onInsertAfter();2937 handled = true;2938 }2939 }2940 else if (keynum == 35) { // End2941 if (altKey) { // Alt+End2942 // find the last node2943 var lastNode = this._lastNode();2944 if (lastNode) {2945 lastNode.focus(Node.focusElement || this._getElementName(target));2946 }2947 handled = true;2948 }2949 }2950 else if (keynum == 36) { // Home2951 if (altKey) { // Alt+Home2952 // find the first node2953 var firstNode = this._firstNode();2954 if (firstNode) {2955 firstNode.focus(Node.focusElement || this._getElementName(target));2956 }2957 handled = true;2958 }2959 }2960 else if (keynum == 37) { // Arrow Left2961 if (altKey && !shiftKey) { // Alt + Arrow Left2962 // move to left element2963 var prevElement = this._previousElement(target);2964 if (prevElement) {2965 this.focus(this._getElementName(prevElement));2966 }2967 handled = true;2968 }2969 else if (altKey && shiftKey) { // Alt + Shift Arrow left2970 if (this.expanded) {2971 var appendDom = this.getAppend();2972 nextDom = appendDom ? appendDom.nextSibling : undefined;2973 }2974 else {2975 var dom = this.getDom();2976 nextDom = dom.nextSibling;2977 }2978 if (nextDom) {2979 nextNode = Node.getNodeFromTarget(nextDom);2980 nextDom2 = nextDom.nextSibling;2981 nextNode2 = Node.getNodeFromTarget(nextDom2);2982 if (nextNode && nextNode instanceof AppendNode &&2983 !(this.parent.childs.length == 1) &&2984 nextNode2 && nextNode2.parent) {2985 nextNode2.parent.moveBefore(this, nextNode2);2986 this.focus(Node.focusElement || this._getElementName(target));2987 }2988 }2989 }2990 }2991 else if (keynum == 38) { // Arrow Up2992 if (altKey && !shiftKey) { // Alt + Arrow Up2993 // find the previous node2994 prevNode = this._previousNode();2995 if (prevNode) {2996 prevNode.focus(Node.focusElement || this._getElementName(target));2997 }2998 handled = true;2999 }3000 else if (altKey && shiftKey) { // Alt + Shift + Arrow Up3001 // find the previous node3002 prevNode = this._previousNode();3003 if (prevNode && prevNode.parent) {3004 prevNode.parent.moveBefore(this, prevNode);3005 this.focus(Node.focusElement || this._getElementName(target));3006 }3007 handled = true;3008 }3009 }3010 else if (keynum == 39) { // Arrow Right3011 if (altKey && !shiftKey) { // Alt + Arrow Right3012 // move to right element3013 var nextElement = this._nextElement(target);3014 if (nextElement) {3015 this.focus(this._getElementName(nextElement));3016 }3017 handled = true;3018 }3019 else if (altKey && shiftKey) { // Alt + Shift Arrow Right3020 dom = this.getDom();3021 var prevDom = dom.previousSibling;3022 if (prevDom) {3023 prevNode = Node.getNodeFromTarget(prevDom);3024 if (prevNode && prevNode.parent &&3025 (prevNode instanceof AppendNode)3026 && !prevNode.isVisible()) {3027 prevNode.parent.moveBefore(this, prevNode);3028 this.focus(Node.focusElement || this._getElementName(target));3029 }3030 }3031 }3032 }3033 else if (keynum == 40) { // Arrow Down3034 if (altKey && !shiftKey) { // Alt + Arrow Down3035 // find the next node3036 nextNode = this._nextNode();3037 if (nextNode) {3038 nextNode.focus(Node.focusElement || this._getElementName(target));3039 }3040 handled = true;3041 }3042 else if (altKey && shiftKey) { // Alt + Shift + Arrow Down3043 // find the 2nd next node and move before that one3044 if (this.expanded) {3045 nextNode = this.append ? this.append._nextNode() : undefined;3046 }3047 else {3048 nextNode = this._nextNode();3049 }3050 nextDom = nextNode ? nextNode.getDom() : undefined;3051 if (this.parent.childs.length == 1) {3052 nextDom2 = nextDom;3053 }3054 else {3055 nextDom2 = nextDom ? nextDom.nextSibling : undefined;3056 }3057 var nextNode2 = Node.getNodeFromTarget(nextDom2);3058 if (nextNode2 && nextNode2.parent) {3059 nextNode2.parent.moveBefore(this, nextNode2);3060 this.focus(Node.focusElement || this._getElementName(target));3061 }3062 handled = true;3063 }3064 }3065 if (handled) {3066 util.preventDefault(event);3067 util.stopPropagation(event);3068 }3069};3070/**3071 * Handle the expand event, when clicked on the expand button3072 * @param {boolean} recurse If true, child nodes will be expanded too3073 * @private3074 */3075Node.prototype._onExpand = function (recurse) {3076 if (recurse) {3077 // Take the table offline3078 var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this3079 var frame = table.parentNode;3080 var scrollTop = frame.scrollTop;3081 frame.removeChild(table);3082 }3083 if (this.expanded) {3084 this.collapse(recurse);3085 }3086 else {3087 this.expand(recurse);3088 }3089 if (recurse) {3090 // Put the table online again3091 frame.appendChild(table);3092 frame.scrollTop = scrollTop;3093 }3094};3095/**3096 * Remove this node3097 * @private3098 */3099Node.prototype._onRemove = function() {3100 this.editor.highlighter.unhighlight();3101 var childs = this.parent.childs;3102 var index = childs.indexOf(this);3103 // adjust the focus3104 var oldSelection = this.editor.getSelection();3105 if (childs[index + 1]) {3106 childs[index + 1].focus();3107 }3108 else if (childs[index - 1]) {3109 childs[index - 1].focus();3110 }3111 else {3112 this.parent.focus();3113 }3114 var newSelection = this.editor.getSelection();3115 // remove the node3116 this.parent._remove(this);3117 // store history action3118 this.editor._onAction('removeNode', {3119 'node': this,3120 'parent': this.parent,3121 'index': index,3122 'oldSelection': oldSelection,3123 'newSelection': newSelection3124 });3125};3126/**3127 * Duplicate this node3128 * @private3129 */3130Node.prototype._onDuplicate = function() {3131 var oldSelection = this.editor.getSelection();3132 var clone = this.parent._duplicate(this);3133 clone.focus();3134 var newSelection = this.editor.getSelection();3135 this.editor._onAction('duplicateNode', {3136 'node': this,3137 'clone': clone,3138 'parent': this.parent,3139 'oldSelection': oldSelection,3140 'newSelection': newSelection3141 });3142};3143/**3144 * Handle insert before event3145 * @param {String} [field]3146 * @param {*} [value]3147 * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'3148 * @private3149 */3150Node.prototype._onInsertBefore = function (field, value, type) {3151 var oldSelection = this.editor.getSelection();3152 var newNode = new Node(this.editor, {3153 'field': (field != undefined) ? field : '',3154 'value': (value != undefined) ? value : '',3155 'type': type3156 });3157 newNode.expand(true);3158 this.parent.insertBefore(newNode, this);3159 this.editor.highlighter.unhighlight();3160 newNode.focus('field');3161 var newSelection = this.editor.getSelection();3162 this.editor._onAction('insertBeforeNode', {3163 'node': newNode,3164 'beforeNode': this,3165 'parent': this.parent,3166 'oldSelection': oldSelection,3167 'newSelection': newSelection3168 });3169};3170/**3171 * Handle insert after event3172 * @param {String} [field]3173 * @param {*} [value]3174 * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'3175 * @private3176 */3177Node.prototype._onInsertAfter = function (field, value, type) {3178 var oldSelection = this.editor.getSelection();3179 var newNode = new Node(this.editor, {3180 'field': (field != undefined) ? field : '',3181 'value': (value != undefined) ? value : '',3182 'type': type3183 });3184 newNode.expand(true);3185 this.parent.insertAfter(newNode, this);3186 this.editor.highlighter.unhighlight();3187 newNode.focus('field');3188 var newSelection = this.editor.getSelection();3189 this.editor._onAction('insertAfterNode', {3190 'node': newNode,3191 'afterNode': this,3192 'parent': this.parent,3193 'oldSelection': oldSelection,3194 'newSelection': newSelection3195 });3196};3197/**3198 * Handle append event3199 * @param {String} [field]3200 * @param {*} [value]3201 * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'3202 * @private3203 */3204Node.prototype._onAppend = function (field, value, type) {3205 var oldSelection = this.editor.getSelection();3206 var newNode = new Node(this.editor, {3207 'field': (field != undefined) ? field : '',3208 'value': (value != undefined) ? value : '',3209 'type': type3210 });3211 newNode.expand(true);3212 this.parent.appendChild(newNode);3213 this.editor.highlighter.unhighlight();3214 newNode.focus('field');3215 var newSelection = this.editor.getSelection();3216 this.editor._onAction('appendNode', {3217 'node': newNode,3218 'parent': this.parent,3219 'oldSelection': oldSelection,3220 'newSelection': newSelection3221 });3222};3223/**3224 * Change the type of the node's value3225 * @param {String} newType3226 * @private3227 */3228Node.prototype._onChangeType = function (newType) {3229 var oldType = this.type;3230 if (newType != oldType) {3231 var oldSelection = this.editor.getSelection();3232 this.changeType(newType);3233 var newSelection = this.editor.getSelection();3234 this.editor._onAction('changeType', {3235 'node': this,3236 'oldType': oldType,3237 'newType': newType,3238 'oldSelection': oldSelection,3239 'newSelection': newSelection3240 });3241 }3242};3243/**3244 * Sort the childs of the node. Only applicable when the node has type 'object'3245 * or 'array'.3246 * @param {String} direction Sorting direction. Available values: "asc", "desc"3247 * @private3248 */3249Node.prototype._onSort = function (direction) {3250 if (this._hasChilds()) {3251 var order = (direction == 'desc') ? -1 : 1;3252 var prop = (this.type == 'array') ? 'value': 'field';3253 this.hideChilds();3254 var oldChilds = this.childs;3255 var oldSort = this.sort;3256 // copy the array (the old one will be kept for an undo action3257 this.childs = this.childs.concat();3258 // sort the arrays3259 this.childs.sort(function (a, b) {3260 if (a[prop] > b[prop]) return order;3261 if (a[prop] < b[prop]) return -order;3262 return 0;3263 });3264 this.sort = (order == 1) ? 'asc' : 'desc';3265 this.editor._onAction('sort', {3266 'node': this,3267 'oldChilds': oldChilds,3268 'oldSort': oldSort,3269 'newChilds': this.childs,3270 'newSort': this.sort3271 });3272 this.showChilds();3273 }3274};3275/**3276 * Create a table row with an append button.3277 * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable3278 */3279Node.prototype.getAppend = function () {3280 if (!this.append) {3281 this.append = new AppendNode(this.editor);3282 this.append.setParent(this);3283 }3284 return this.append.getDom();3285};3286/**3287 * Find the node from an event target3288 * @param {Node} target3289 * @return {Node | undefined} node or undefined when not found3290 * @static3291 */3292Node.getNodeFromTarget = function (target) {3293 while (target) {3294 if (target.node) {3295 return target.node;3296 }3297 target = target.parentNode;3298 }3299 return undefined;3300};3301/**3302 * Get the previously rendered node3303 * @return {Node | null} previousNode3304 * @private3305 */3306Node.prototype._previousNode = function () {3307 var prevNode = null;3308 var dom = this.getDom();3309 if (dom && dom.parentNode) {3310 // find the previous field3311 var prevDom = dom;3312 do {3313 prevDom = prevDom.previousSibling;3314 prevNode = Node.getNodeFromTarget(prevDom);3315 }3316 while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));3317 }3318 return prevNode;3319};3320/**3321 * Get the next rendered node3322 * @return {Node | null} nextNode3323 * @private3324 */3325Node.prototype._nextNode = function () {3326 var nextNode = null;3327 var dom = this.getDom();3328 if (dom && dom.parentNode) {3329 // find the previous field3330 var nextDom = dom;3331 do {3332 nextDom = nextDom.nextSibling;3333 nextNode = Node.getNodeFromTarget(nextDom);3334 }3335 while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));3336 }3337 return nextNode;3338};3339/**3340 * Get the first rendered node3341 * @return {Node | null} firstNode3342 * @private3343 */3344Node.prototype._firstNode = function () {3345 var firstNode = null;3346 var dom = this.getDom();3347 if (dom && dom.parentNode) {3348 var firstDom = dom.parentNode.firstChild;3349 firstNode = Node.getNodeFromTarget(firstDom);3350 }3351 return firstNode;3352};3353/**3354 * Get the last rendered node3355 * @return {Node | null} lastNode3356 * @private3357 */3358Node.prototype._lastNode = function () {3359 var lastNode = null;3360 var dom = this.getDom();3361 if (dom && dom.parentNode) {3362 var lastDom = dom.parentNode.lastChild;3363 lastNode = Node.getNodeFromTarget(lastDom);3364 while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {3365 lastDom = lastDom.previousSibling;3366 lastNode = Node.getNodeFromTarget(lastDom);3367 }3368 }3369 return lastNode;3370};3371/**3372 * Get the next element which can have focus.3373 * @param {Element} elem3374 * @return {Element | null} nextElem3375 * @private3376 */3377Node.prototype._previousElement = function (elem) {3378 var dom = this.dom;3379 // noinspection FallthroughInSwitchStatementJS3380 switch (elem) {3381 case dom.value:3382 if (this.fieldEditable) {3383 return dom.field;3384 }3385 // intentional fall through3386 case dom.field:3387 if (this._hasChilds()) {3388 return dom.expand;3389 }3390 // intentional fall through3391 case dom.expand:3392 return dom.menu;3393 case dom.menu:3394 if (dom.drag) {3395 return dom.drag;3396 }3397 // intentional fall through3398 default:3399 return null;3400 }3401};3402/**3403 * Get the next element which can have focus.3404 * @param {Element} elem3405 * @return {Element | null} nextElem3406 * @private3407 */3408Node.prototype._nextElement = function (elem) {3409 var dom = this.dom;3410 // noinspection FallthroughInSwitchStatementJS3411 switch (elem) {3412 case dom.drag:3413 return dom.menu;3414 case dom.menu:3415 if (this._hasChilds()) {3416 return dom.expand;3417 }3418 // intentional fall through3419 case dom.expand:3420 if (this.fieldEditable) {3421 return dom.field;3422 }3423 // intentional fall through3424 case dom.field:3425 if (!this._hasChilds()) {3426 return dom.value;3427 }3428 default:3429 return null;3430 }3431};3432/**3433 * Get the dom name of given element. returns null if not found.3434 * For example when element == dom.field, "field" is returned.3435 * @param {Element} element3436 * @return {String | null} elementName Available elements with name: 'drag',3437 * 'menu', 'expand', 'field', 'value'3438 * @private3439 */3440Node.prototype._getElementName = function (element) {3441 var dom = this.dom;3442 for (var name in dom) {3443 if (dom.hasOwnProperty(name)) {3444 if (dom[name] == element) {3445 return name;3446 }3447 }3448 }3449 return null;3450};3451/**3452 * Test if this node has childs. This is the case when the node is an object3453 * or array.3454 * @return {boolean} hasChilds3455 * @private3456 */3457Node.prototype._hasChilds = function () {3458 return this.type == 'array' || this.type == 'object';3459};3460// titles with explanation for the different types3461Node.TYPE_TITLES = {3462 'auto': 'Field type "auto". ' +3463 'The field type is automatically determined from the value ' +3464 'and can be a string, number, boolean, or null.',3465 'object': 'Field type "object". ' +3466 'An object contains an unordered set of key/value pairs.',3467 'array': 'Field type "array". ' +3468 'An array contains an ordered collection of values.',3469 'string': 'Field type "string". ' +3470 'Field type is not determined from the value, ' +3471 'but always returned as string.'3472};3473/**3474 * Show a contextmenu for this node3475 * @param {HTMLElement} anchor Anchor element to attache the context menu to.3476 * @param {function} [onClose] Callback method called when the context menu3477 * is being closed.3478 */3479Node.prototype.showContextMenu = function (anchor, onClose) {3480 var node = this;3481 var titles = Node.TYPE_TITLES;3482 var items = [];3483 items.push({3484 'text': 'Type',3485 'title': 'Change the type of this field',3486 'className': 'type-' + this.type,3487 'submenu': [3488 {3489 'text': 'Auto',3490 'className': 'type-auto' +3491 (this.type == 'auto' ? ' selected' : ''),3492 'title': titles.auto,3493 'click': function () {3494 node._onChangeType('auto');3495 }3496 },3497 {3498 'text': 'Array',3499 'className': 'type-array' +3500 (this.type == 'array' ? ' selected' : ''),3501 'title': titles.array,3502 'click': function () {3503 node._onChangeType('array');3504 }3505 },3506 {3507 'text': 'Object',3508 'className': 'type-object' +3509 (this.type == 'object' ? ' selected' : ''),3510 'title': titles.object,3511 'click': function () {3512 node._onChangeType('object');3513 }3514 },3515 {3516 'text': 'String',3517 'className': 'type-string' +3518 (this.type == 'string' ? ' selected' : ''),3519 'title': titles.string,3520 'click': function () {3521 node._onChangeType('string');3522 }3523 }3524 ]3525 });3526 if (this._hasChilds()) {3527 var direction = ((this.sort == 'asc') ? 'desc': 'asc');3528 items.push({3529 'text': 'Sort',3530 'title': 'Sort the childs of this ' + this.type,3531 'className': 'sort-' + direction,3532 'click': function () {3533 node._onSort(direction);3534 },3535 'submenu': [3536 {3537 'text': 'Ascending',3538 'className': 'sort-asc',3539 'title': 'Sort the childs of this ' + this.type + ' in ascending order',3540 'click': function () {3541 node._onSort('asc');3542 }3543 },3544 {3545 'text': 'Descending',3546 'className': 'sort-desc',3547 'title': 'Sort the childs of this ' + this.type +' in descending order',3548 'click': function () {3549 node._onSort('desc');3550 }3551 }3552 ]3553 });3554 }3555 if (this.parent && this.parent._hasChilds()) {3556 // create a separator3557 items.push({3558 'type': 'separator'3559 });3560 // create append button (for last child node only)3561 var childs = node.parent.childs;3562 if (node == childs[childs.length - 1]) {3563 items.push({3564 'text': 'Append',3565 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',3566 'submenuTitle': 'Select the type of the field to be appended',3567 'className': 'append',3568 'click': function () {3569 node._onAppend('', '', 'auto');3570 },3571 'submenu': [3572 {3573 'text': 'Auto',3574 'className': 'type-auto',3575 'title': titles.auto,3576 'click': function () {3577 node._onAppend('', '', 'auto');3578 }3579 },3580 {3581 'text': 'Array',3582 'className': 'type-array',3583 'title': titles.array,3584 'click': function () {3585 node._onAppend('', []);3586 }3587 },3588 {3589 'text': 'Object',3590 'className': 'type-object',3591 'title': titles.object,3592 'click': function () {3593 node._onAppend('', {});3594 }3595 },3596 {3597 'text': 'String',3598 'className': 'type-string',3599 'title': titles.string,3600 'click': function () {3601 node._onAppend('', '', 'string');3602 }3603 }3604 ]3605 });3606 }3607 // create insert button3608 items.push({3609 'text': 'Insert',3610 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',3611 'submenuTitle': 'Select the type of the field to be inserted',3612 'className': 'insert',3613 'click': function () {3614 node._onInsertBefore('', '', 'auto');3615 },3616 'submenu': [3617 {3618 'text': 'Auto',3619 'className': 'type-auto',3620 'title': titles.auto,3621 'click': function () {3622 node._onInsertBefore('', '', 'auto');3623 }3624 },3625 {3626 'text': 'Array',3627 'className': 'type-array',3628 'title': titles.array,3629 'click': function () {3630 node._onInsertBefore('', []);3631 }3632 },3633 {3634 'text': 'Object',3635 'className': 'type-object',3636 'title': titles.object,3637 'click': function () {3638 node._onInsertBefore('', {});3639 }3640 },3641 {3642 'text': 'String',3643 'className': 'type-string',3644 'title': titles.string,3645 'click': function () {3646 node._onInsertBefore('', '', 'string');3647 }3648 }3649 ]3650 });3651 // create duplicate button3652 items.push({3653 'text': 'Duplicate',3654 'title': 'Duplicate this field (Ctrl+D)',3655 'className': 'duplicate',3656 'click': function () {3657 node._onDuplicate();3658 }3659 });3660 // create remove button3661 items.push({3662 'text': 'Remove',3663 'title': 'Remove this field (Ctrl+Del)',3664 'className': 'remove',3665 'click': function () {3666 node._onRemove();3667 }3668 });3669 }3670 var menu = new ContextMenu(items, {close: onClose});3671 menu.show(anchor);3672};3673/**3674 * get the type of a value3675 * @param {*} value3676 * @return {String} type Can be 'object', 'array', 'string', 'auto'3677 * @private3678 */3679Node.prototype._getType = function(value) {3680 if (value instanceof Array) {3681 return 'array';3682 }3683 if (value instanceof Object) {3684 return 'object';3685 }3686 if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {3687 return 'string';3688 }3689 return 'auto';3690};3691/**3692 * cast contents of a string to the correct type. This can be a string,3693 * a number, a boolean, etc3694 * @param {String} str3695 * @return {*} castedStr3696 * @private3697 */3698Node.prototype._stringCast = function(str) {3699 var lower = str.toLowerCase(),3700 num = Number(str), // will nicely fail with '123ab'3701 numFloat = parseFloat(str); // will nicely fail with ' '3702 if (str == '') {3703 return '';3704 }3705 else if (lower == 'null') {3706 return null;3707 }3708 else if (lower == 'true') {3709 return true;3710 }3711 else if (lower == 'false') {3712 return false;3713 }3714 else if (!isNaN(num) && !isNaN(numFloat)) {3715 return num;3716 }3717 else {3718 return str;3719 }3720};3721/**3722 * escape a text, such that it can be displayed safely in an HTML element3723 * @param {String} text3724 * @return {String} escapedText3725 * @private3726 */3727Node.prototype._escapeHTML = function (text) {3728 var htmlEscaped = String(text)3729 .replace(/</g, '<')3730 .replace(/>/g, '>')3731 .replace(/ /g, ' ') // replace double space with an nbsp and space3732 .replace(/^ /, ' ') // space at start3733 .replace(/ $/, ' '); // space at end3734 var json = JSON.stringify(htmlEscaped);3735 return json.substring(1, json.length - 1);3736};3737/**3738 * unescape a string.3739 * @param {String} escapedText3740 * @return {String} text3741 * @private3742 */3743Node.prototype._unescapeHTML = function (escapedText) {3744 var json = '"' + this._escapeJSON(escapedText) + '"';3745 var htmlEscaped = util.parse(json);3746 return htmlEscaped3747 .replace(/</g, '<')3748 .replace(/>/g, '>')3749 .replace(/ /g, ' ');3750};3751/**3752 * escape a text to make it a valid JSON string. The method will:3753 * - replace unescaped double quotes with '\"'3754 * - replace unescaped backslash with '\\'3755 * - replace returns with '\n'3756 * @param {String} text3757 * @return {String} escapedText3758 * @private3759 */3760Node.prototype._escapeJSON = function (text) {3761 // TODO: replace with some smart regex (only when a new solution is faster!)3762 var escaped = '';3763 var i = 0, iMax = text.length;3764 while (i < iMax) {3765 var c = text.charAt(i);3766 if (c == '\n') {3767 escaped += '\\n';3768 }3769 else if (c == '\\') {3770 escaped += c;3771 i++;3772 c = text.charAt(i);3773 if ('"\\/bfnrtu'.indexOf(c) == -1) {3774 escaped += '\\'; // no valid escape character3775 }3776 escaped += c;3777 }3778 else if (c == '"') {3779 escaped += '\\"';3780 }3781 else {3782 escaped += c;3783 }3784 i++;3785 }3786 return escaped;3787};3788/**3789 * @constructor AppendNode3790 * @extends Node3791 * @param {TreeEditor} editor3792 * Create a new AppendNode. This is a special node which is created at the3793 * end of the list with childs for an object or array3794 */3795function AppendNode (editor) {3796 /** @type {TreeEditor} */3797 this.editor = editor;3798 this.dom = {};3799}3800AppendNode.prototype = new Node();3801/**3802 * Return a table row with an append button.3803 * @return {Element} dom TR element3804 */3805AppendNode.prototype.getDom = function () {3806 // TODO: implement a new solution for the append node3807 var dom = this.dom;3808 if (dom.tr) {3809 return dom.tr;3810 }3811 // a row for the append button3812 var trAppend = document.createElement('tr');3813 trAppend.node = this;3814 dom.tr = trAppend;3815 // TODO: consistent naming3816 if (this.editor.mode.edit) {3817 // a cell for the dragarea column3818 dom.tdDrag = document.createElement('td');3819 // create context menu3820 var tdMenu = document.createElement('td');3821 dom.tdMenu = tdMenu;3822 var menu = document.createElement('button');3823 menu.className = 'contextmenu';3824 menu.title = 'Click to open the actions menu (Ctrl+M)';3825 dom.menu = menu;3826 tdMenu.appendChild(dom.menu);3827 }3828 // a cell for the contents (showing text 'empty')3829 var tdAppend = document.createElement('td');3830 var domText = document.createElement('div');3831 domText.innerHTML = '(empty)';3832 domText.className = 'readonly';3833 tdAppend.appendChild(domText);3834 dom.td = tdAppend;3835 dom.text = domText;3836 this.updateDom();3837 return trAppend;3838};3839/**3840 * Update the HTML dom of the Node3841 */3842AppendNode.prototype.updateDom = function () {3843 var dom = this.dom;3844 var tdAppend = dom.td;3845 if (tdAppend) {3846 tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';3847 // TODO: not so nice hard coded offset3848 }3849 var domText = dom.text;3850 if (domText) {3851 domText.innerHTML = '(empty ' + this.parent.type + ')';3852 }3853 // attach or detach the contents of the append node:3854 // hide when the parent has childs, show when the parent has no childs3855 var trAppend = dom.tr;3856 if (!this.isVisible()) {3857 if (dom.tr.firstChild) {3858 if (dom.tdDrag) {3859 trAppend.removeChild(dom.tdDrag);3860 }3861 if (dom.tdMenu) {3862 trAppend.removeChild(dom.tdMenu);3863 }3864 trAppend.removeChild(tdAppend);3865 }3866 }3867 else {3868 if (!dom.tr.firstChild) {3869 if (dom.tdDrag) {3870 trAppend.appendChild(dom.tdDrag);3871 }3872 if (dom.tdMenu) {3873 trAppend.appendChild(dom.tdMenu);3874 }3875 trAppend.appendChild(tdAppend);3876 }3877 }3878};3879/**3880 * Check whether the AppendNode is currently visible.3881 * the AppendNode is visible when its parent has no childs (i.e. is empty).3882 * @return {boolean} isVisible3883 */3884AppendNode.prototype.isVisible = function () {3885 return (this.parent.childs.length == 0);3886};3887/**3888 * Show a contextmenu for this node3889 * @param {HTMLElement} anchor The element to attach the menu to.3890 * @param {function} [onClose] Callback method called when the context menu3891 * is being closed.3892 */3893AppendNode.prototype.showContextMenu = function (anchor, onClose) {3894 var node = this;3895 var titles = Node.TYPE_TITLES;3896 var items = [3897 // create append button3898 {3899 'text': 'Append',3900 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',3901 'submenuTitle': 'Select the type of the field to be appended',3902 'className': 'insert',3903 'click': function () {3904 node._onAppend('', '', 'auto');3905 },3906 'submenu': [3907 {3908 'text': 'Auto',3909 'className': 'type-auto',3910 'title': titles.auto,3911 'click': function () {3912 node._onAppend('', '', 'auto');3913 }3914 },3915 {3916 'text': 'Array',3917 'className': 'type-array',3918 'title': titles.array,3919 'click': function () {3920 node._onAppend('', []);3921 }3922 },3923 {3924 'text': 'Object',3925 'className': 'type-object',3926 'title': titles.object,3927 'click': function () {3928 node._onAppend('', {});3929 }3930 },3931 {3932 'text': 'String',3933 'className': 'type-string',3934 'title': titles.string,3935 'click': function () {3936 node._onAppend('', '', 'string');3937 }3938 }3939 ]3940 }3941 ];3942 var menu = new ContextMenu(items, {close: onClose});3943 menu.show(anchor);3944};3945/**3946 * Handle an event. The event is catched centrally by the editor3947 * @param {Event} event3948 */3949AppendNode.prototype.onEvent = function (event) {3950 var type = event.type;3951 var target = event.target || event.srcElement;3952 var dom = this.dom;3953 // highlight the append nodes parent3954 var menu = dom.menu;3955 if (target == menu) {3956 if (type == 'mouseover') {3957 this.editor.highlighter.highlight(this.parent);3958 }3959 else if (type == 'mouseout') {3960 this.editor.highlighter.unhighlight();3961 }3962 }3963 // context menu events3964 if (type == 'click' && target == dom.menu) {3965 var highlighter = this.editor.highlighter;3966 highlighter.highlight(this.parent);3967 highlighter.lock();3968 util.addClassName(dom.menu, 'selected');3969 this.showContextMenu(dom.menu, function () {3970 util.removeClassName(dom.menu, 'selected');3971 highlighter.unlock();3972 highlighter.unhighlight();3973 });3974 }3975 if (type == 'keydown') {3976 this.onKeyDown(event);3977 }3978};3979/**3980 * A context menu3981 * @param {Object[]} items Array containing the menu structure3982 * TODO: describe structure3983 * @param {Object} [options] Object with options. Available options:3984 * {function} close Callback called when the3985 * context menu is being closed.3986 * @constructor3987 */3988function ContextMenu (items, options) {3989 this.dom = {};3990 var me = this;3991 var dom = this.dom;3992 this.anchor = undefined;3993 this.items = items;3994 this.eventListeners = {};3995 this.selection = undefined; // holds the selection before the menu was opened3996 this.visibleSubmenu = undefined;3997 this.onClose = options ? options.close : undefined;3998 // create a container element3999 var menu = document.createElement('div');4000 menu.className = 'jsoneditor-contextmenu';4001 dom.menu = menu;4002 // create a list to hold the menu items4003 var list = document.createElement('ul');4004 list.className = 'menu';4005 menu.appendChild(list);4006 dom.list = list;4007 dom.items = []; // list with all buttons4008 // create a (non-visible) button to set the focus to the menu4009 var focusButton = document.createElement('button');4010 dom.focusButton = focusButton;4011 var li = document.createElement('li');4012 li.style.overflow = 'hidden';4013 li.style.height = '0';4014 li.appendChild(focusButton);4015 list.appendChild(li);4016 function createMenuItems (list, domItems, items) {4017 items.forEach(function (item) {4018 if (item.type == 'separator') {4019 // create a separator4020 var separator = document.createElement('div');4021 separator.className = 'separator';4022 li = document.createElement('li');4023 li.appendChild(separator);4024 list.appendChild(li);4025 }4026 else {4027 var domItem = {};4028 // create a menu item4029 var li = document.createElement('li');4030 list.appendChild(li);4031 // create a button in the menu item4032 var button = document.createElement('button');4033 button.className = item.className;4034 domItem.button = button;4035 if (item.title) {4036 button.title = item.title;4037 }4038 if (item.click) {4039 button.onclick = function () {4040 me.hide();4041 item.click();4042 };4043 }4044 li.appendChild(button);4045 // create the contents of the button4046 if (item.submenu) {4047 // add the icon to the button4048 var divIcon = document.createElement('div');4049 divIcon.className = 'icon';4050 button.appendChild(divIcon);4051 button.appendChild(document.createTextNode(item.text));4052 var buttonSubmenu;4053 if (item.click) {4054 // submenu and a button with a click handler4055 button.className += ' default';4056 var buttonExpand = document.createElement('button');4057 domItem.buttonExpand = buttonExpand;4058 buttonExpand.className = 'expand';4059 buttonExpand.innerHTML = '<div class="expand"></div>';4060 li.appendChild(buttonExpand);4061 if (item.submenuTitle) {4062 buttonExpand.title = item.submenuTitle;4063 }4064 buttonSubmenu = buttonExpand;4065 }4066 else {4067 // submenu and a button without a click handler4068 var divExpand = document.createElement('div');4069 divExpand.className = 'expand';4070 button.appendChild(divExpand);4071 buttonSubmenu = button;4072 }4073 // attach a handler to expand/collapse the submenu4074 buttonSubmenu.onclick = function () {4075 me._onExpandItem(domItem);4076 buttonSubmenu.focus();4077 };4078 // create the submenu4079 var domSubItems = [];4080 domItem.subItems = domSubItems;4081 var ul = document.createElement('ul');4082 domItem.ul = ul;4083 ul.className = 'menu';4084 ul.style.height = '0';4085 li.appendChild(ul);4086 createMenuItems(ul, domSubItems, item.submenu);4087 }4088 else {4089 // no submenu, just a button with clickhandler4090 button.innerHTML = '<div class="icon"></div>' + item.text;4091 }4092 domItems.push(domItem);4093 }4094 });4095 }4096 createMenuItems(list, this.dom.items, items);4097 // TODO: when the editor is small, show the submenu on the right instead of inline?4098 // calculate the max height of the menu with one submenu expanded4099 this.maxHeight = 0; // height in pixels4100 items.forEach(function (item) {4101 var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;4102 me.maxHeight = Math.max(me.maxHeight, height);4103 });4104}4105/**4106 * Get the currently visible buttons4107 * @return {Array.<HTMLElement>} buttons4108 * @private4109 */4110ContextMenu.prototype._getVisibleButtons = function () {4111 var buttons = [];4112 var me = this;4113 this.dom.items.forEach(function (item) {4114 buttons.push(item.button);4115 if (item.buttonExpand) {4116 buttons.push(item.buttonExpand);4117 }4118 if (item.subItems && item == me.expandedItem) {4119 item.subItems.forEach(function (subItem) {4120 buttons.push(subItem.button);4121 if (subItem.buttonExpand) {4122 buttons.push(subItem.buttonExpand);4123 }4124 // TODO: change to fully recursive method4125 });4126 }4127 });4128 return buttons;4129};4130// currently displayed context menu, a singleton. We may only have one visible context menu4131ContextMenu.visibleMenu = undefined;4132function scrollPosition(){4133 if(window.pageYOffset){4134 return window.pageYOffset;4135 }4136 else {4137 return Math.max(document.body.scrollTop,document.documentElement.scrollTop);4138 }4139}4140/**4141 * Attach the menu to an anchor4142 * @param {HTMLElement} anchor4143 */4144ContextMenu.prototype.show = function (anchor) {4145 this.hide();4146 // calculate whether the menu fits below the anchor4147 var windowHeight = util.getWindowHeight();4148 var anchorHeight = anchor.offsetHeight;4149 var menuHeight = this.maxHeight;4150 // position the menu4151 var left = util.getAbsoluteLeft(anchor);4152 var top = util.getAbsoluteTop(anchor);4153 // display the menu below the anchor4154 this.dom.menu.style.left = left + 'px';4155 this.dom.menu.style.top = (top + anchorHeight - scrollPosition()) + 'px';4156 this.dom.menu.style.bottom = '';4157 // attach the menu to the document4158 document.body.appendChild(this.dom.menu);4159 // create and attach event listeners4160 var me = this;4161 var list = this.dom.list;4162 this.eventListeners.mousedown = util.addEventListener(4163 document, 'mousedown', function (event) {4164 // hide menu on click outside of the menu4165 event = event || window.event;4166 var target = event.target || event.srcElement;4167 if ((target != list) && !me._isChildOf(target, list)) {4168 me.hide();4169 util.stopPropagation(event);4170 util.preventDefault(event);4171 }4172 });4173 this.eventListeners.mousewheel = util.addEventListener(4174 document, 'mousewheel', function () {4175 // hide the menu on mouse scroll4176 util.stopPropagation(event);4177 util.preventDefault(event);4178 });4179 this.eventListeners.keydown = util.addEventListener(4180 document, 'keydown', function (event) {4181 me._onKeyDown(event);4182 });4183 // move focus to the first button in the context menu4184 this.selection = util.getSelection();4185 this.anchor = anchor;4186 setTimeout(function () {4187 me.dom.focusButton.focus();4188 }, 0);4189 if (ContextMenu.visibleMenu) {4190 ContextMenu.visibleMenu.hide();4191 }4192 ContextMenu.visibleMenu = this;4193};4194/**4195 * Hide the context menu if visible4196 */4197ContextMenu.prototype.hide = function () {4198 // remove the menu from the DOM4199 if (this.dom.menu.parentNode) {4200 this.dom.menu.parentNode.removeChild(this.dom.menu);4201 if (this.onClose) {4202 this.onClose();4203 }4204 }4205 // remove all event listeners4206 // all event listeners are supposed to be attached to document.4207 for (var name in this.eventListeners) {4208 if (this.eventListeners.hasOwnProperty(name)) {4209 var fn = this.eventListeners[name];4210 if (fn) {4211 util.removeEventListener(document, name, fn);4212 }4213 delete this.eventListeners[name];4214 }4215 }4216 if (ContextMenu.visibleMenu == this) {4217 ContextMenu.visibleMenu = undefined;4218 }4219};4220/**4221 * Expand a submenu4222 * Any currently expanded submenu will be hided.4223 * @param {Object} domItem4224 * @private4225 */4226ContextMenu.prototype._onExpandItem = function (domItem) {4227 var me = this;4228 var alreadyVisible = (domItem == this.expandedItem);4229 // hide the currently visible submenu4230 var expandedItem = this.expandedItem;4231 if (expandedItem) {4232 //var ul = expandedItem.ul;4233 expandedItem.ul.style.height = '0';4234 expandedItem.ul.style.padding = '';4235 setTimeout(function () {4236 if (me.expandedItem != expandedItem) {4237 expandedItem.ul.style.display = '';4238 util.removeClassName(expandedItem.ul.parentNode, 'selected');4239 }4240 }, 300); // timeout duration must match the css transition duration4241 this.expandedItem = undefined;4242 }4243 if (!alreadyVisible) {4244 var ul = domItem.ul;4245 ul.style.display = 'block';4246 var height = ul.clientHeight; // force a reflow in Firefox4247 setTimeout(function () {4248 if (me.expandedItem == domItem) {4249 ul.style.height = (ul.childNodes.length * 24) + 'px';4250 ul.style.padding = '5px 10px';4251 }4252 }, 0);4253 util.addClassName(ul.parentNode, 'selected');4254 this.expandedItem = domItem;4255 }4256};4257/**4258 * Handle onkeydown event4259 * @param {Event} event4260 * @private4261 */4262ContextMenu.prototype._onKeyDown = function (event) {4263 event = event || window.event;4264 var target = event.target || event.srcElement;4265 var keynum = event.which || event.keyCode;4266 var handled = false;4267 var buttons, targetIndex, prevButton, nextButton;4268 if (keynum == 27) { // ESC4269 // hide the menu on ESC key4270 // restore previous selection and focus4271 if (this.selection) {4272 util.setSelection(this.selection);4273 }4274 if (this.anchor) {4275 this.anchor.focus();4276 }4277 this.hide();4278 handled = true;4279 }4280 else if (keynum == 9) { // Tab4281 if (!event.shiftKey) { // Tab4282 buttons = this._getVisibleButtons();4283 targetIndex = buttons.indexOf(target);4284 if (targetIndex == buttons.length - 1) {4285 // move to first button4286 buttons[0].focus();4287 handled = true;4288 }4289 }4290 else { // Shift+Tab4291 buttons = this._getVisibleButtons();4292 targetIndex = buttons.indexOf(target);4293 if (targetIndex == 0) {4294 // move to last button4295 buttons[buttons.length - 1].focus();4296 handled = true;4297 }4298 }4299 }4300 else if (keynum == 37) { // Arrow Left4301 if (target.className == 'expand') {4302 buttons = this._getVisibleButtons();4303 targetIndex = buttons.indexOf(target);4304 prevButton = buttons[targetIndex - 1];4305 if (prevButton) {4306 prevButton.focus();4307 }4308 }4309 handled = true;4310 }4311 else if (keynum == 38) { // Arrow Up4312 buttons = this._getVisibleButtons();4313 targetIndex = buttons.indexOf(target);4314 prevButton = buttons[targetIndex - 1];4315 if (prevButton && prevButton.className == 'expand') {4316 // skip expand button4317 prevButton = buttons[targetIndex - 2];4318 }4319 if (!prevButton) {4320 // move to last button4321 prevButton = buttons[buttons.length - 1];4322 }4323 if (prevButton) {4324 prevButton.focus();4325 }4326 handled = true;4327 }4328 else if (keynum == 39) { // Arrow Right4329 buttons = this._getVisibleButtons();4330 targetIndex = buttons.indexOf(target);4331 nextButton = buttons[targetIndex + 1];4332 if (nextButton && nextButton.className == 'expand') {4333 nextButton.focus();4334 }4335 handled = true;4336 }4337 else if (keynum == 40) { // Arrow Down4338 buttons = this._getVisibleButtons();4339 targetIndex = buttons.indexOf(target);4340 nextButton = buttons[targetIndex + 1];4341 if (nextButton && nextButton.className == 'expand') {4342 // skip expand button4343 nextButton = buttons[targetIndex + 2];4344 }4345 if (!nextButton) {4346 // move to first button4347 nextButton = buttons[0];4348 }4349 if (nextButton) {4350 nextButton.focus();4351 handled = true;4352 }4353 handled = true;4354 }4355 // TODO: arrow left and right4356 if (handled) {4357 util.stopPropagation(event);4358 util.preventDefault(event);4359 }4360};4361/**4362 * Test if an element is a child of a parent element.4363 * @param {Element} child4364 * @param {Element} parent4365 * @return {boolean} isChild4366 */4367ContextMenu.prototype._isChildOf = function (child, parent) {4368 var e = child.parentNode;4369 while (e) {4370 if (e == parent) {4371 return true;4372 }4373 e = e.parentNode;4374 }4375 return false;4376};4377/**4378 * @constructor History4379 * Store action history, enables undo and redo4380 * @param {JSONEditor} editor4381 */4382function History (editor) {4383 this.editor = editor;4384 this.clear();4385 // map with all supported actions4386 this.actions = {4387 'editField': {4388 'undo': function (params) {4389 params.node.updateField(params.oldValue);4390 },4391 'redo': function (params) {4392 params.node.updateField(params.newValue);4393 }4394 },4395 'editValue': {4396 'undo': function (params) {4397 params.node.updateValue(params.oldValue);4398 },4399 'redo': function (params) {4400 params.node.updateValue(params.newValue);4401 }4402 },4403 'appendNode': {4404 'undo': function (params) {4405 params.parent.removeChild(params.node);4406 },4407 'redo': function (params) {4408 params.parent.appendChild(params.node);4409 }4410 },4411 'insertBeforeNode': {4412 'undo': function (params) {4413 params.parent.removeChild(params.node);4414 },4415 'redo': function (params) {4416 params.parent.insertBefore(params.node, params.beforeNode);4417 }4418 },4419 'insertAfterNode': {4420 'undo': function (params) {4421 params.parent.removeChild(params.node);4422 },4423 'redo': function (params) {4424 params.parent.insertAfter(params.node, params.afterNode);4425 }4426 },4427 'removeNode': {4428 'undo': function (params) {4429 var parent = params.parent;4430 var beforeNode = parent.childs[params.index] || parent.append;4431 parent.insertBefore(params.node, beforeNode);4432 },4433 'redo': function (params) {4434 params.parent.removeChild(params.node);4435 }4436 },4437 'duplicateNode': {4438 'undo': function (params) {4439 params.parent.removeChild(params.clone);4440 },4441 'redo': function (params) {4442 params.parent.insertAfter(params.clone, params.node);4443 }4444 },4445 'changeType': {4446 'undo': function (params) {4447 params.node.changeType(params.oldType);4448 },4449 'redo': function (params) {4450 params.node.changeType(params.newType);4451 }4452 },4453 'moveNode': {4454 'undo': function (params) {4455 params.startParent.moveTo(params.node, params.startIndex);4456 },4457 'redo': function (params) {4458 params.endParent.moveTo(params.node, params.endIndex);4459 }4460 },4461 'sort': {4462 'undo': function (params) {4463 var node = params.node;4464 node.hideChilds();4465 node.sort = params.oldSort;4466 node.childs = params.oldChilds;4467 node.showChilds();4468 },4469 'redo': function (params) {4470 var node = params.node;4471 node.hideChilds();4472 node.sort = params.newSort;4473 node.childs = params.newChilds;4474 node.showChilds();4475 }4476 }4477 // TODO: restore the original caret position and selection with each undo4478 // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"4479 };4480}4481/**4482 * The method onChange is executed when the History is changed, and can4483 * be overloaded.4484 */4485History.prototype.onChange = function () {};4486/**4487 * Add a new action to the history4488 * @param {String} action The executed action. Available actions: "editField",4489 * "editValue", "changeType", "appendNode",4490 * "removeNode", "duplicateNode", "moveNode"4491 * @param {Object} params Object containing parameters describing the change.4492 * The parameters in params depend on the action (for4493 * example for "editValue" the Node, old value, and new4494 * value are provided). params contains all information4495 * needed to undo or redo the action.4496 */4497History.prototype.add = function (action, params) {4498 this.index++;4499 this.history[this.index] = {4500 'action': action,4501 'params': params,4502 'timestamp': new Date()4503 };4504 // remove redo actions which are invalid now4505 if (this.index < this.history.length - 1) {4506 this.history.splice(this.index + 1, this.history.length - this.index - 1);4507 }4508 // fire onchange event4509 this.onChange();4510};4511/**4512 * Clear history4513 */4514History.prototype.clear = function () {4515 this.history = [];4516 this.index = -1;4517 // fire onchange event4518 this.onChange();4519};4520/**4521 * Check if there is an action available for undo4522 * @return {Boolean} canUndo4523 */4524History.prototype.canUndo = function () {4525 return (this.index >= 0);4526};4527/**4528 * Check if there is an action available for redo4529 * @return {Boolean} canRedo4530 */4531History.prototype.canRedo = function () {4532 return (this.index < this.history.length - 1);4533};4534/**4535 * Undo the last action4536 */4537History.prototype.undo = function () {4538 if (this.canUndo()) {4539 var obj = this.history[this.index];4540 if (obj) {4541 var action = this.actions[obj.action];4542 if (action && action.undo) {4543 action.undo(obj.params);4544 if (obj.params.oldSelection) {4545 this.editor.setSelection(obj.params.oldSelection);4546 }4547 }4548 else {4549 util.log('Error: unknown action "' + obj.action + '"');4550 }4551 }4552 this.index--;4553 // fire onchange event4554 this.onChange();4555 }4556};4557/**4558 * Redo the last action4559 */4560History.prototype.redo = function () {4561 if (this.canRedo()) {4562 this.index++;4563 var obj = this.history[this.index];4564 if (obj) {4565 var action = this.actions[obj.action];4566 if (action && action.redo) {4567 action.redo(obj.params);4568 if (obj.params.newSelection) {4569 this.editor.setSelection(obj.params.newSelection);4570 }4571 }4572 else {4573 util.log('Error: unknown action "' + obj.action + '"');4574 }4575 }4576 // fire onchange event4577 this.onChange();4578 }4579};4580/**4581 * create a mode box to be used in the editor menu's4582 * @param {JSONEditor} editor4583 * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'4584 * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'4585 * @returns {HTMLElement} box4586 */4587function createModeBox(editor, modes, current) {4588 /**4589 * Switch the mode of the editor4590 * @param {String} mode4591 */4592 function switchMode(mode) {4593 // switch mode4594 editor.setMode(mode);4595 // restore focus on mode box4596 var modeBox = editor.dom && editor.dom.modeBox;4597 if (modeBox) {4598 modeBox.focus();4599 }4600 }4601 // available modes4602 var availableModes = {4603 code: {4604 'text': 'Code',4605 'title': 'Switch to code highlighter',4606 'click': function () {4607 switchMode('code')4608 }4609 },4610 form: {4611 'text': 'Form',4612 'title': 'Switch to form editor',4613 'click': function () {4614 switchMode('form');4615 }4616 },4617 text: {4618 'text': 'Text',4619 'title': 'Switch to plain text editor',4620 'click': function () {4621 switchMode('text');4622 }4623 },4624 tree: {4625 'text': 'Tree',4626 'title': 'Switch to tree editor',4627 'click': function () {4628 switchMode('tree');4629 }4630 },4631 view: {4632 'text': 'View',4633 'title': 'Switch to tree view',4634 'click': function () {4635 switchMode('view');4636 }4637 }4638 };4639 // list the selected modes4640 var items = [];4641 for (var i = 0; i < modes.length; i++) {4642 var mode = modes[i];4643 var item = availableModes[mode];4644 if (!item) {4645 throw new Error('Unknown mode "' + mode + '"');4646 }4647 item.className = 'type-modes' + ((current == mode) ? ' selected' : '');4648 items.push(item);4649 }4650 // retrieve the title of current mode4651 var currentMode = availableModes[current];4652 if (!currentMode) {4653 throw new Error('Unknown mode "' + current + '"');4654 }4655 var currentTitle = currentMode.text;4656 // create the html element4657 var box = document.createElement('button');4658 box.className = 'modes separator';4659 box.innerHTML = currentTitle + ' ▾';4660 box.title = 'Switch editor mode';4661 box.onclick = function () {4662 var menu = new ContextMenu(items);4663 menu.show(box);4664 };4665 return box;4666}4667/**4668 * @constructor SearchBox4669 * Create a search box in given HTML container4670 * @param {JSONEditor} editor The JSON Editor to attach to4671 * @param {Element} container HTML container element of where to4672 * create the search box4673 */4674function SearchBox (editor, container) {4675 var searchBox = this;4676 this.editor = editor;4677 this.timeout = undefined;4678 this.delay = 200; // ms4679 this.lastText = undefined;4680 this.dom = {};4681 this.dom.container = container;4682 var table = document.createElement('table');4683 this.dom.table = table;4684 table.className = 'search';4685 container.appendChild(table);4686 var tbody = document.createElement('tbody');4687 this.dom.tbody = tbody;4688 table.appendChild(tbody);4689 var tr = document.createElement('tr');4690 tbody.appendChild(tr);4691 var td = document.createElement('td');4692 tr.appendChild(td);4693 var results = document.createElement('div');4694 this.dom.results = results;4695 results.className = 'results';4696 td.appendChild(results);4697 td = document.createElement('td');4698 tr.appendChild(td);4699 var divInput = document.createElement('div');4700 this.dom.input = divInput;4701 divInput.className = 'frame';4702 divInput.title = 'Search fields and values';4703 td.appendChild(divInput);4704 // table to contain the text input and search button4705 var tableInput = document.createElement('table');4706 divInput.appendChild(tableInput);4707 var tbodySearch = document.createElement('tbody');4708 tableInput.appendChild(tbodySearch);4709 tr = document.createElement('tr');4710 tbodySearch.appendChild(tr);4711 var refreshSearch = document.createElement('button');4712 refreshSearch.className = 'refresh';4713 td = document.createElement('td');4714 td.appendChild(refreshSearch);4715 tr.appendChild(td);4716 var search = document.createElement('input');4717 this.dom.search = search;4718 search.oninput = function (event) {4719 searchBox._onDelayedSearch(event);4720 };4721 search.onchange = function (event) { // For IE 84722 searchBox._onSearch(event);4723 };4724 search.onkeydown = function (event) {4725 searchBox._onKeyDown(event);4726 };4727 search.onkeyup = function (event) {4728 searchBox._onKeyUp(event);4729 };4730 refreshSearch.onclick = function (event) {4731 search.select();4732 };4733 // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=5988194734 td = document.createElement('td');4735 td.appendChild(search);4736 tr.appendChild(td);4737 var searchNext = document.createElement('button');4738 searchNext.title = 'Next result (Enter)';4739 searchNext.className = 'next';4740 searchNext.onclick = function () {4741 searchBox.next();4742 };4743 td = document.createElement('td');4744 td.appendChild(searchNext);4745 tr.appendChild(td);4746 var searchPrevious = document.createElement('button');4747 searchPrevious.title = 'Previous result (Shift+Enter)';4748 searchPrevious.className = 'previous';4749 searchPrevious.onclick = function () {4750 searchBox.previous();4751 };4752 td = document.createElement('td');4753 td.appendChild(searchPrevious);4754 tr.appendChild(td);4755}4756/**4757 * Go to the next search result4758 * @param {boolean} [focus] If true, focus will be set to the next result4759 * focus is false by default.4760 */4761SearchBox.prototype.next = function(focus) {4762 if (this.results != undefined) {4763 var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;4764 if (index > this.results.length - 1) {4765 index = 0;4766 }4767 this._setActiveResult(index, focus);4768 }4769};4770/**4771 * Go to the prevous search result4772 * @param {boolean} [focus] If true, focus will be set to the next result4773 * focus is false by default.4774 */4775SearchBox.prototype.previous = function(focus) {4776 if (this.results != undefined) {4777 var max = this.results.length - 1;4778 var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;4779 if (index < 0) {4780 index = max;4781 }4782 this._setActiveResult(index, focus);4783 }4784};4785/**4786 * Set new value for the current active result4787 * @param {Number} index4788 * @param {boolean} [focus] If true, focus will be set to the next result.4789 * focus is false by default.4790 * @private4791 */4792SearchBox.prototype._setActiveResult = function(index, focus) {4793 // de-activate current active result4794 if (this.activeResult) {4795 var prevNode = this.activeResult.node;4796 var prevElem = this.activeResult.elem;4797 if (prevElem == 'field') {4798 delete prevNode.searchFieldActive;4799 }4800 else {4801 delete prevNode.searchValueActive;4802 }4803 prevNode.updateDom();4804 }4805 if (!this.results || !this.results[index]) {4806 // out of range, set to undefined4807 this.resultIndex = undefined;4808 this.activeResult = undefined;4809 return;4810 }4811 this.resultIndex = index;4812 // set new node active4813 var node = this.results[this.resultIndex].node;4814 var elem = this.results[this.resultIndex].elem;4815 if (elem == 'field') {4816 node.searchFieldActive = true;4817 }4818 else {4819 node.searchValueActive = true;4820 }4821 this.activeResult = this.results[this.resultIndex];4822 node.updateDom();4823 // TODO: not so nice that the focus is only set after the animation is finished4824 node.scrollTo(function () {4825 if (focus) {4826 node.focus(elem);4827 }4828 });4829};4830/**4831 * Cancel any running onDelayedSearch.4832 * @private4833 */4834SearchBox.prototype._clearDelay = function() {4835 if (this.timeout != undefined) {4836 clearTimeout(this.timeout);4837 delete this.timeout;4838 }4839};4840/**4841 * Start a timer to execute a search after a short delay.4842 * Used for reducing the number of searches while typing.4843 * @param {Event} event4844 * @private4845 */4846SearchBox.prototype._onDelayedSearch = function (event) {4847 // execute the search after a short delay (reduces the number of4848 // search actions while typing in the search text box)4849 this._clearDelay();4850 var searchBox = this;4851 this.timeout = setTimeout(function (event) {4852 searchBox._onSearch(event);4853 },4854 this.delay);4855};4856/**4857 * Handle onSearch event4858 * @param {Event} event4859 * @param {boolean} [forceSearch] If true, search will be executed again even4860 * when the search text is not changed.4861 * Default is false.4862 * @private4863 */4864SearchBox.prototype._onSearch = function (event, forceSearch) {4865 this._clearDelay();4866 var value = this.dom.search.value;4867 var text = (value.length > 0) ? value : undefined;4868 if (text != this.lastText || forceSearch) {4869 // only search again when changed4870 this.lastText = text;4871 this.results = this.editor.search(text);4872 this._setActiveResult(undefined);4873 // display search results4874 if (text != undefined) {4875 var resultCount = this.results.length;4876 switch (resultCount) {4877 case 0: this.dom.results.innerHTML = 'no results'; break;4878 case 1: this.dom.results.innerHTML = '1 result'; break;4879 default: this.dom.results.innerHTML = resultCount + ' results'; break;4880 }4881 }4882 else {4883 this.dom.results.innerHTML = '';4884 }4885 }4886};4887/**4888 * Handle onKeyDown event in the input box4889 * @param {Event} event4890 * @private4891 */4892SearchBox.prototype._onKeyDown = function (event) {4893 event = event || window.event;4894 var keynum = event.which || event.keyCode;4895 if (keynum == 27) { // ESC4896 this.dom.search.value = ''; // clear search4897 this._onSearch(event);4898 util.preventDefault(event);4899 util.stopPropagation(event);4900 }4901 else if (keynum == 13) { // Enter4902 if (event.ctrlKey) {4903 // force to search again4904 this._onSearch(event, true);4905 }4906 else if (event.shiftKey) {4907 // move to the previous search result4908 this.previous();4909 }4910 else {4911 // move to the next search result4912 this.next();4913 }4914 util.preventDefault(event);4915 util.stopPropagation(event);4916 }4917};4918/**4919 * Handle onKeyUp event in the input box4920 * @param {Event} event4921 * @private4922 */4923SearchBox.prototype._onKeyUp = function (event) {4924 event = event || window.event;4925 var keynum = event.which || event.keyCode;4926 if (keynum != 27 && keynum != 13) { // !show and !Enter4927 this._onDelayedSearch(event); // For IE 84928 }4929};4930/**4931 * The highlighter can highlight/unhighlight a node, and4932 * animate the visibility of a context menu.4933 * @constructor Highlighter4934 */4935function Highlighter () {4936 this.locked = false;4937}4938/**4939 * Hightlight given node and its childs4940 * @param {Node} node4941 */4942Highlighter.prototype.highlight = function (node) {4943 if (this.locked) {4944 return;4945 }4946 if (this.node != node) {4947 // unhighlight current node4948 if (this.node) {4949 this.node.setHighlight(false);4950 }4951 // highlight new node4952 this.node = node;4953 this.node.setHighlight(true);4954 }4955 // cancel any current timeout4956 this._cancelUnhighlight();4957};4958/**4959 * Unhighlight currently highlighted node.4960 * Will be done after a delay4961 */4962Highlighter.prototype.unhighlight = function () {4963 if (this.locked) {4964 return;4965 }4966 var me = this;4967 if (this.node) {4968 this._cancelUnhighlight();4969 // do the unhighlighting after a small delay, to prevent re-highlighting4970 // the same node when moving from the drag-icon to the contextmenu-icon4971 // or vice versa.4972 this.unhighlightTimer = setTimeout(function () {4973 me.node.setHighlight(false);4974 me.node = undefined;4975 me.unhighlightTimer = undefined;4976 }, 0);4977 }4978};4979/**4980 * Cancel an unhighlight action (if before the timeout of the unhighlight action)4981 * @private4982 */4983Highlighter.prototype._cancelUnhighlight = function () {4984 if (this.unhighlightTimer) {4985 clearTimeout(this.unhighlightTimer);4986 this.unhighlightTimer = undefined;4987 }4988};4989/**4990 * Lock highlighting or unhighlighting nodes.4991 * methods highlight and unhighlight do not work while locked.4992 */4993Highlighter.prototype.lock = function () {4994 this.locked = true;4995};4996/**4997 * Unlock highlighting or unhighlighting nodes4998 */4999Highlighter.prototype.unlock = function () {5000 this.locked = false;5001};5002// create namespace5003util = {};5004// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/5005if(!Array.prototype.indexOf) {5006 Array.prototype.indexOf = function(obj){5007 for(var i = 0; i < this.length; i++){5008 if(this[i] == obj){5009 return i;5010 }5011 }5012 return -1;5013 }5014}5015// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach5016if (!Array.prototype.forEach) {5017 Array.prototype.forEach = function(fn, scope) {5018 for(var i = 0, len = this.length; i < len; ++i) {5019 fn.call(scope || this, this[i], i, this);5020 }5021 }5022}5023// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray5024if(!Array.isArray) {5025 Array.isArray = function (vArg) {5026 return Object.prototype.toString.call(vArg) === "[object Array]";5027 };5028}5029/**5030 * Parse JSON using the parser built-in in the browser.5031 * On exception, the jsonString is validated and a detailed error is thrown.5032 * @param {String} jsonString5033 */5034util.parse = function parse(jsonString) {5035 try {5036 return JSON.parse(jsonString);5037 }5038 catch (err) {5039 // try to throw a more detailed error message using validate5040 util.validate(jsonString);5041 throw err;5042 }5043};5044/**5045 * Validate a string containing a JSON object5046 * This method uses JSONLint to validate the String. If JSONLint is not5047 * available, the built-in JSON parser of the browser is used.5048 * @param {String} jsonString String with an (invalid) JSON object5049 * @throws Error5050 */5051util.validate = function validate(jsonString) {5052 if (typeof(jsonlint) != 'undefined') {5053 jsonlint.parse(jsonString);5054 }5055 else {5056 JSON.parse(jsonString);5057 }5058};5059/**5060 * Extend object a with the properties of object b5061 * @param {Object} a5062 * @param {Object} b5063 * @return {Object} a5064 */5065util.extend = function extend(a, b) {5066 for (var prop in b) {5067 if (b.hasOwnProperty(prop)) {5068 a[prop] = b[prop];5069 }5070 }5071 return a;5072};5073/**5074 * Remove all properties from object a5075 * @param {Object} a5076 * @return {Object} a5077 */5078util.clear = function clear (a) {5079 for (var prop in a) {5080 if (a.hasOwnProperty(prop)) {5081 delete a[prop];5082 }5083 }5084 return a;5085};5086/**5087 * Output text to the console, if console is available5088 * @param {...*} args5089 */5090util.log = function log (args) {5091 if (console && typeof console.log === 'function') {5092 console.log.apply(console, arguments);5093 }5094};5095/**5096 * Get the type of an object5097 * @param {*} object5098 * @return {String} type5099 */5100util.type = function type (object) {5101 if (object === null) {5102 return 'null';5103 }5104 if (object === undefined) {5105 return 'undefined';5106 }5107 if ((object instanceof Number) || (typeof object === 'number')) {5108 return 'number';5109 }5110 if ((object instanceof String) || (typeof object === 'string')) {5111 return 'string';5112 }5113 if ((object instanceof Boolean) || (typeof object === 'boolean')) {5114 return 'boolean';5115 }5116 if ((object instanceof RegExp) || (typeof object === 'regexp')) {5117 return 'regexp';5118 }5119 if (Array.isArray(object)) {5120 return 'array';5121 }5122 return 'object';5123};5124/**5125 * Test whether a text contains a url (matches when a string starts5126 * with 'http://*' or 'https://*' and has no whitespace characters)5127 * @param {String} text5128 */5129var isUrlRegex = /^https?:\/\/\S+$/;5130util.isUrl = function isUrl (text) {5131 return (typeof text == 'string' || text instanceof String) &&5132 isUrlRegex.test(text);5133};5134/**5135 * Retrieve the absolute left value of a DOM element5136 * @param {Element} elem A dom element, for example a div5137 * @return {Number} left The absolute left position of this element5138 * in the browser page.5139 */5140util.getAbsoluteLeft = function getAbsoluteLeft(elem) {5141 var left = elem.offsetLeft;5142 var body = document.body;5143 var e = elem.offsetParent;5144 while (e != null && elem != body) {5145 left += e.offsetLeft;5146 left -= e.scrollLeft;5147 e = e.offsetParent;5148 }5149 return left;5150};5151/**5152 * Retrieve the absolute top value of a DOM element5153 * @param {Element} elem A dom element, for example a div5154 * @return {Number} top The absolute top position of this element5155 * in the browser page.5156 */5157util.getAbsoluteTop = function getAbsoluteTop(elem) {5158 var top = elem.offsetTop;5159 var body = document.body;5160 var e = elem.offsetParent;5161 while (e != null && e != body && e.localName!="li") {5162 top += e.offsetTop;5163 top -= e.scrollTop;5164 e = e.offsetParent;5165 }5166 return top;5167};5168/**5169 * Get the absolute, vertical mouse position from an event.5170 * @param {Event} event5171 * @return {Number} mouseY5172 */5173util.getMouseY = function getMouseY(event) {5174 var mouseY;5175 if ('pageY' in event) {5176 mouseY = event.pageY;5177 }5178 else {5179 // for IE8 and older5180 mouseY = (event.clientY + document.documentElement.scrollTop);5181 }5182 return mouseY;5183};5184/**5185 * Get the absolute, horizontal mouse position from an event.5186 * @param {Event} event5187 * @return {Number} mouseX5188 */5189util.getMouseX = function getMouseX(event) {5190 var mouseX;5191 if ('pageX' in event) {5192 mouseX = event.pageX;5193 }5194 else {5195 // for IE8 and older5196 mouseX = (event.clientX + document.documentElement.scrollLeft);5197 }5198 return mouseX;5199};5200/**5201 * Get the window height5202 * @return {Number} windowHeight5203 */5204util.getWindowHeight = function getWindowHeight() {5205 if ('innerHeight' in window) {5206 return window.innerHeight;5207 }5208 else {5209 // for IE8 and older5210 return Math.max(document.body.clientHeight,5211 document.documentElement.clientHeight);5212 }5213};5214/**5215 * add a className to the given elements style5216 * @param {Element} elem5217 * @param {String} className5218 */5219util.addClassName = function addClassName(elem, className) {5220 var classes = elem.className.split(' ');5221 if (classes.indexOf(className) == -1) {5222 classes.push(className); // add the class to the array5223 elem.className = classes.join(' ');5224 }5225};5226/**5227 * add a className to the given elements style5228 * @param {Element} elem5229 * @param {String} className5230 */5231util.removeClassName = function removeClassName(elem, className) {5232 var classes = elem.className.split(' ');5233 var index = classes.indexOf(className);5234 if (index != -1) {5235 classes.splice(index, 1); // remove the class from the array5236 elem.className = classes.join(' ');5237 }5238};5239/**5240 * Strip the formatting from the contents of a div5241 * the formatting from the div itself is not stripped, only from its childs.5242 * @param {Element} divElement5243 */5244util.stripFormatting = function stripFormatting(divElement) {5245 var childs = divElement.childNodes;5246 for (var i = 0, iMax = childs.length; i < iMax; i++) {5247 var child = childs[i];5248 // remove the style5249 if (child.style) {5250 // TODO: test if child.attributes does contain style5251 child.removeAttribute('style');5252 }5253 // remove all attributes5254 var attributes = child.attributes;5255 if (attributes) {5256 for (var j = attributes.length - 1; j >= 0; j--) {5257 var attribute = attributes[j];5258 if (attribute.specified == true) {5259 child.removeAttribute(attribute.name);5260 }5261 }5262 }5263 // recursively strip childs5264 util.stripFormatting(child);5265 }5266};5267/**5268 * Set focus to the end of an editable div5269 * code from Nico Burns5270 * http://stackoverflow.com/users/140293/nico-burns5271 * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity5272 * @param {Element} contentEditableElement A content editable div5273 */5274util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {5275 var range, selection;5276 if(document.createRange) {//Firefox, Chrome, Opera, Safari, IE 9+5277 range = document.createRange();//Create a range (a range is a like the selection but invisible)5278 range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range5279 range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start5280 selection = window.getSelection();//get the selection object (allows you to change selection)5281 selection.removeAllRanges();//remove any selections already made5282 selection.addRange(range);//make the range you have just created the visible selection5283 }5284 else if(document.selection) {//IE 8 and lower5285 range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)5286 range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range5287 range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start5288 range.select();//Select the range (make it the visible selection5289 }5290};5291/**5292 * Select all text of a content editable div.5293 * http://stackoverflow.com/a/3806004/12627535294 * @param {Element} contentEditableElement A content editable div5295 */5296util.selectContentEditable = function selectContentEditable(contentEditableElement) {5297 if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {5298 return;5299 }5300 var sel, range;5301 if (window.getSelection && document.createRange) {5302 range = document.createRange();5303 range.selectNodeContents(contentEditableElement);5304 sel = window.getSelection();5305 sel.removeAllRanges();5306 sel.addRange(range);5307 } else if (document.body.createTextRange) {5308 range = document.body.createTextRange();5309 range.moveToElementText(contentEditableElement);5310 range.select();...
text_selection.js
Source:text_selection.js
...100 }101 }102 return backward;103 }104 function selectContentEditable(el, from, to, needFocus, inverse) {105 var endPosition = null,106 firstTextNodeChild = null,107 latestTextNodeChild = null,108 startPosition = null,109 temp = null;110 if (typeof from !== 'undefined' && typeof to !== 'undefined' && from > to) {111 temp = from;112 from = to;113 to = temp;114 inverse = true;115 }116 if (typeof from === 'undefined') {117 firstTextNodeChild = ContentEditableHelper.getFirstVisibleTextNode(el);118 startPosition = {119 node: firstTextNodeChild || el,120 offset: firstTextNodeChild && firstTextNodeChild.nodeValue ?121 ContentEditableHelper.getFirstNonWhitespaceSymbolIndex(firstTextNodeChild.nodeValue) : 0122 };123 }124 if (typeof to === 'undefined') {125 latestTextNodeChild = ContentEditableHelper.getLastVisibleTextNode(el, true);126 endPosition = {127 node: latestTextNodeChild || el,128 offset: latestTextNodeChild && latestTextNodeChild.nodeValue ?129 ContentEditableHelper.getLastNonWhitespaceSymbolIndex(latestTextNodeChild.nodeValue) : 0130 };131 }132 startPosition = startPosition || ContentEditableHelper.calculateNodeAndOffsetByPosition(el, from);133 endPosition = endPosition || ContentEditableHelper.calculateNodeAndOffsetByPosition(el, to);134 if (!startPosition.node || !endPosition.node)135 return;136 exports.selectByNodesAndOffsets(startPosition.node, startPosition.offset, endPosition.node, endPosition.offset, needFocus, inverse);137 }138 function correctContentEditableSelectionBeforeDelete(el) {139 var selection = exports.getSelectionByElement(el),140 startNode = selection.anchorNode,141 endNode = selection.focusNode,142 startOffset = selection.anchorOffset,143 endOffset = selection.focusOffset,144 startNodeFirstNonWhitespaceSymbol = ContentEditableHelper.getFirstNonWhitespaceSymbolIndex(startNode.nodeValue),145 startNodeLastNonWhitespaceSymbol = ContentEditableHelper.getLastNonWhitespaceSymbolIndex(startNode.nodeValue),146 endNodeFirstNonWhitespaceSymbol = ContentEditableHelper.getFirstNonWhitespaceSymbolIndex(endNode.nodeValue),147 endNodeLastNonWhitespaceSymbol = ContentEditableHelper.getLastNonWhitespaceSymbolIndex(endNode.nodeValue),148 newStartOffset = null,149 newEndOffset = null;150 if (startNode.nodeType === 3) {151 if (startOffset < startNodeFirstNonWhitespaceSymbol && startOffset !== 0)152 newStartOffset = 0;153 else if (startOffset !== startNode.nodeValue.length && ((ContentEditableHelper.isInvisibleTextNode(startNode) && startOffset !== 0) ||154 (startOffset > startNodeLastNonWhitespaceSymbol)))155 newStartOffset = startNode.nodeValue.length;156 }157 if (endNode.nodeType === 3) {158 if (endOffset < endNodeFirstNonWhitespaceSymbol && endOffset !== 0)159 newEndOffset = 0;160 else if (endOffset !== endNode.nodeValue.length && ((ContentEditableHelper.isInvisibleTextNode(endNode) && endOffset !== 0) ||161 (endOffset > endNodeLastNonWhitespaceSymbol)))162 newEndOffset = endNode.nodeValue.length;163 }164 if ($.browser.webkit) {165 if (newStartOffset !== null) {166 if (newStartOffset === 0)167 startNode.nodeValue = startNode.nodeValue.substring(startNodeFirstNonWhitespaceSymbol);168 else169 startNode.nodeValue = startNode.nodeValue.substring(0, startNodeLastNonWhitespaceSymbol);170 }171 if (newEndOffset !== null) {172 if (newEndOffset === 0)173 endNode.nodeValue = endNode.nodeValue.substring(endNodeFirstNonWhitespaceSymbol);174 else175 endNode.nodeValue = endNode.nodeValue.substring(0, endNodeLastNonWhitespaceSymbol);176 }177 }178 if (newStartOffset !== null || newEndOffset !== null) {179 newStartOffset = newStartOffset !== null ? (newStartOffset === 0 ? newStartOffset : startNode.nodeValue.length) : startOffset;180 newEndOffset = newEndOffset !== null ? (newEndOffset === 0 ? newEndOffset : endNode.nodeValue.length) : endOffset;181 exports.selectByNodesAndOffsets(startNode, newStartOffset, endNode, newEndOffset);182 }183 }184 function correctRectangle(currentRect, options) {185 var documentScroll = options.documentScroll,186 iFrameDocumentScroll = options.iFrameDocumentScroll,187 iFrameOffset = options.iFrameOffset,188 iFramePadding = options.iFramePadding,189 iFrameBorders = options.iFrameBorders,190 currentRectHeight = currentRect.top + options.elementHeight - 1,191 clientOffset = null,192 currentLeft = null,193 currentTop = null,194 currentBottom = null;195 if (Util.isIE && !Util.isIE11 && options.isInProcessedIFrame) {196 if (Util.browserVersion === 9 && !options.isContentEditable) {197 currentLeft = Math.ceil(currentRect.left) + options.windowTopScroll.left - options.crossDomainIFrameOffset.left - options.crossDomainIFrameBorders.left - options.crossDomainIFramePadding.left;198 currentTop = Math.ceil(currentRect.top) + options.windowTopScroll.top - options.crossDomainIFrameOffset.top - options.crossDomainIFrameBorders.top - options.crossDomainIFramePadding.top;199 currentBottom = Math.ceil(currentRect.bottom) + options.windowTopScroll.top - options.crossDomainIFrameOffset.top - options.crossDomainIFrameBorders.top - options.crossDomainIFramePadding.top;200 } else if (Util.browserVersion === 10 || options.isContentEditable) {201 currentLeft = Math.ceil(currentRect.left);202 currentTop = Math.ceil(currentRect.top);203 currentBottom = Math.ceil(currentRect.bottom);204 }205 } else {206 if (options.isTextarea) {207 currentLeft = Math.ceil(currentRect.left);208 currentTop = Math.ceil(currentRect.top);209 currentBottom = Math.ceil(currentRect.bottom);210 } else {211 if (options.isInIFrame && (options.isContentEditable || Util.isIE)) {212 clientOffset = options.elementOffset;213 clientOffset.left -= (iFrameOffset.left + iFrameBorders.left + iFramePadding.left);214 clientOffset.top -= (iFrameOffset.top + iFrameBorders.top + iFramePadding.top);215 clientOffset = Util.offsetToClientCoords({x: clientOffset.left, y: clientOffset.top});216 } else217 clientOffset = Util.offsetToClientCoords({x: options.elementOffset.left, y: options.elementOffset.top});218 currentLeft = Math.ceil(Math.ceil(currentRect.left) <= clientOffset.x ? clientOffset.x + options.elementBorders.left + 1 : currentRect.left);219 currentTop = Math.ceil(Math.ceil(currentRect.top) <= clientOffset.y ? clientOffset.y + options.elementBorders.top + 1 : currentRect.top);220 currentBottom = Math.floor(Math.floor(currentRect.bottom) >= (clientOffset.y + options.elementBorders.top + options.elementBorders.bottom + options.elementHeight) ? currentRectHeight : currentRect.bottom);221 }222 }223 if (options.isInIFrame && (options.isContentEditable || (Util.isIE && Util.browserVersion !== 9))) {224 currentLeft = currentLeft + iFrameDocumentScroll.left + iFrameOffset.left + iFrameBorders.left + iFramePadding.left;225 currentTop = currentTop + iFrameDocumentScroll.top + iFrameOffset.top + iFrameBorders.top + iFramePadding.top;226 currentBottom = currentBottom + iFrameDocumentScroll.top + iFrameOffset.top + iFrameBorders.top + iFramePadding.top;227 } else if (options.isInIFrame && Util.isIE && Util.browserVersion === 9) {228 currentLeft = currentLeft + iFrameDocumentScroll.left + documentScroll.left;229 currentTop = currentTop + iFrameDocumentScroll.top + documentScroll.top;230 currentBottom = currentBottom + iFrameDocumentScroll.top + documentScroll.top;231 } else if (options.isContentEditable || (Util.isIE && !Util.isIE11)) {232 currentLeft = currentLeft + documentScroll.left;233 currentTop = currentTop + documentScroll.top;234 currentBottom = currentBottom + documentScroll.top;235 } else {236 currentLeft = currentLeft + documentScroll.left + iFrameDocumentScroll.left;237 currentTop = currentTop + documentScroll.top + iFrameDocumentScroll.top;238 currentBottom = currentBottom + documentScroll.top + iFrameDocumentScroll.top;239 }240 return {241 bottom: currentBottom,242 left: currentLeft,243 top: currentTop244 };245 }246 //API247 exports.getSelectionStart = function (el) {248 var selection = null;249 if (!Util.isContentEditableElement(el))250 return EventSandbox.getSelection(el).start;251 if (exports.hasElementContainsSelection(el)) {252 selection = exports.getSelectionByElement(el);253 return ContentEditableHelper.getSelectionStartPosition(el, selection, hasInverseSelectionContentEditable(el));254 }255 return 0;256 };257 exports.getSelectionEnd = function (el) {258 var selection = null;259 if (!Util.isContentEditableElement(el))260 return EventSandbox.getSelection(el).end;261 if (exports.hasElementContainsSelection(el)) {262 selection = exports.getSelectionByElement(el);263 return ContentEditableHelper.getSelectionEndPosition(el, selection, hasInverseSelectionContentEditable(el));264 }265 return 0;266 };267 exports.getSelectedText = function (el) {268 return el.value.substring(exports.getSelectionStart(el), exports.getSelectionEnd(el));269 };270 exports.hasInverseSelection = function (el) {271 if (Util.isContentEditableElement(el))272 return hasInverseSelectionContentEditable(el);273 return (EventSandbox.getSelection(el).direction || selectionDirection) === BACKWARD_SELECTION_DIRECTION;274 };275 exports.hasInverseSelectionContentEditable = hasInverseSelectionContentEditable;276 exports.getSelectionByElement = function (el) {277 var currentDocument = Util.findDocument(el);278 return currentDocument ? currentDocument.getSelection() : window.getSelection();279 };280 exports.getPositionCoordinates = function (el, position, correctOptions) {281 var range = null,282 rects = null,283 selectionPosition = null,284 rect = null,285 isTextarea = el.tagName.toLowerCase() === 'textarea',286 isContentEditable = Util.isContentEditableElement(el),287 offset = Util.getOffsetPosition(el);288 //NOTE: we don't create fake div element for contentEditable elements289 //because we can get the selection dimensions directly290 if (isContentEditable) {291 range = Util.findDocument(el).createRange();292 selectionPosition = ContentEditableHelper.calculateNodeAndOffsetByPosition(el, position);293 range.setStart(selectionPosition.node, Math.min(selectionPosition.offset, selectionPosition.node.length));294 range.setEnd(selectionPosition.node, Math.min(selectionPosition.offset, selectionPosition.node.length));295 rect = range.getClientRects()[0];296 return rect ? correctRectangle(rect, correctOptions) : null;297 }298 //NOTE: for IE299 if (typeof el.createTextRange === "function") {300 range = el.createTextRange();301 range.collapse(true);302 range.moveStart('character', position);303 range.moveEnd('character', position);304 range.collapse(true);305 rect = range.getBoundingClientRect();306 return rect ? correctRectangle(rect, correctOptions) : null;307 }308 var $body = $(document).find('body'),309 bodyMargin = Util.getElementMargin($body),310 bodyLeft = null,311 bodyTop = null,312 elementMargin = Util.getElementMargin($(el)),313 elementTop = offset.top - elementMargin.top,314 elementLeft = offset.left - elementMargin.left,315 width = el.scrollWidth,316 $fakeDiv = $('<div></div>'),317 fakeDivCssStyles = 'white-space:pre-wrap;border-style:solid;',318 listOfModifiers = ['direction', 'font-family', 'font-size', 'font-size-adjust', 'font-variant', 'font-weight', 'font-style', 'letter-spacing', 'line-height', 'text-align', 'text-indent', 'text-transform', 'word-wrap', 'word-spacing', 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', 'margin-top', 'margin-left', 'margin-right', 'margin-bottom', 'border-top-width', 'border-left-width', 'border-right-width', 'border-bottom-width'];319 if (Util.getCssStyleValue($body[0], 'position') === 'absolute') {320 elementLeft -= bodyMargin.left;321 elementTop -= bodyMargin.top;322 bodyLeft = Util.getCssStyleValue($body[0], 'left');323 if (bodyLeft !== 'auto')324 elementLeft -= parseInt(bodyLeft.replace('px', ''));325 bodyTop = Util.getCssStyleValue($body[0], 'top');326 if (bodyTop !== 'auto')327 elementTop -= parseInt(bodyTop.replace('px', ''));328 }329 $.each(listOfModifiers, function (index, value) {330 fakeDivCssStyles += value + ':' + Util.getCssStyleValue(el, value) + ';';331 });332 $fakeDiv.appendTo($body);333 try {334 $fakeDiv.css({335 cssText: fakeDivCssStyles,336 position: 'absolute',337 top: elementTop,338 left: elementLeft,339 width: width,340 height: el.scrollHeight341 });342 $fakeDiv[0].textContent = !el.value.length ? ' ' : el.value;343 range = document.createRange(); //B254723344 range.setStart($fakeDiv[0].firstChild, Math.min(position, el.value.length));345 range.setEnd($fakeDiv[0].firstChild, Math.min(position, el.value.length));346 if (isTextarea) {347 rects = range.getClientRects();348 rect = range.getBoundingClientRect();349 if (rect.width === 0 && rect.height === 0)350 rect = rects[0];351 } else352 rect = range.getClientRects()[0];353 $fakeDiv.remove();354 } catch (err) {355 $fakeDiv.remove();356 return {};357 }358 return rect ? correctRectangle(rect, correctOptions) : null;359 };360 exports.select = function (el, from, to, inverse) {361 if (Util.isContentEditableElement(el)) {362 selectContentEditable(el, from, to, true, inverse);363 return;364 }365 var start = from || 0,366 end = typeof to === 'undefined' ? el.value.length : to,367 temp = null;368 if (start > end) {369 temp = start;370 start = end;371 end = temp;372 inverse = true;373 }374 EventSandbox.setSelection(el, start, end, inverse ? BACKWARD_SELECTION_DIRECTION : FORWARD_SELECTION_DIRECTION);375 selectionDirection = from === to ?376 NONE_SELECTION_DIRECTION :377 inverse ? BACKWARD_SELECTION_DIRECTION : FORWARD_SELECTION_DIRECTION;378 };379 exports.selectByNodesAndOffsets = function (startNode, startOffset, endNode, endOffset, needFocus, inverse) {380 var parentElement = ContentEditableHelper.findContentEditableParent(startNode),381 curDocument = Util.findDocument(parentElement),382 selection = exports.getSelectionByElement(parentElement),383 range = curDocument.createRange(),384 startNodeLength = startNode.nodeValue ? startNode.length : 0,385 endNodeLength = endNode.nodeValue ? endNode.length : 0;386 var selectionSetter = function () {387 selection.removeAllRanges();388 //NOTE: For IE we can't create inverse selection389 if (!inverse || Util.isIE) {390 range.setStart(startNode, Math.min(startNodeLength, startOffset));391 range.setEnd(endNode, Math.min(endNodeLength, endOffset));392 selection.addRange(range);393 } else {394 range.setStart(endNode, Math.min(endNodeLength, endOffset));395 range.setEnd(endNode, Math.min(endNodeLength, endOffset));396 selection.addRange(range);397 if ($.browser.webkit && ContentEditableHelper.isInvisibleTextNode(startNode)) {398 try {399 selection.extend(startNode, Math.min(startOffset, 1));400 } catch (err) {401 selection.extend(startNode, 0);402 }403 } else404 selection.extend(startNode, Math.min(startNodeLength, startOffset));405 }406 };407 EventSandbox.wrapSetterSelection(parentElement, selectionSetter, needFocus, true);408 };409 exports.deleteSelectionContents = function (el, selectAll) {410 var startSelection = exports.getSelectionStart(el),411 endSelection = exports.getSelectionEnd(el);412 function deleteSelectionRanges(el) {413 var selection = exports.getSelectionByElement(el),414 rangeCount = selection.rangeCount;415 if (!rangeCount)416 return;417 for (var i = 0; i < rangeCount; i++)418 selection.getRangeAt(i).deleteContents();419 }420 if (selectAll)421 selectContentEditable(el);422 if (startSelection === endSelection)423 return;424 // NOTE: If selection is not contain initial and final invisible symbols425 //we should select its426 correctContentEditableSelectionBeforeDelete(el);427 deleteSelectionRanges(el);428 var selection = exports.getSelectionByElement(el),429 range = null;430 //NOTE: We should try to do selection collapsed431 if (selection.rangeCount && !selection.getRangeAt(0).collapsed) {432 range = selection.getRangeAt(0);433 range.collapse(true);434 }435 };436 exports.setCursorToLastVisiblePosition = function (el) {437 var position = ContentEditableHelper.getLastVisiblePosition(el);438 selectContentEditable(el, position, position);439 };440 exports.hasElementContainsSelection = function (el) {441 var selection = exports.getSelectionByElement(el);442 return selection.anchorNode && selection.focusNode ?443 Util.isElementContainsNode(el, selection.anchorNode) && Util.isElementContainsNode(el, selection.focusNode) :444 false;445 };...
text-selection.js
Source:text-selection.js
...249 return currentDocument ? currentDocument.getSelection() : window.getSelection();250}251export function select (el, from, to) {252 if (domUtils.isContentEditableElement(el)) {253 selectContentEditable(el, from, to, true);254 return;255 }256 var start = from || 0;257 var end = typeof to === 'undefined' ? domUtils.getElementValue(el).length : to;258 var inverse = false;259 var temp = null;260 if (start > end) {261 temp = start;262 start = end;263 end = temp;264 inverse = true;265 }266 selectionSandbox.setSelection(el, start, end, inverse ? BACKWARD_SELECTION_DIRECTION : FORWARD_SELECTION_DIRECTION);267 if (from === to)268 selectionDirection = NONE_SELECTION_DIRECTION;269 else270 selectionDirection = inverse ? BACKWARD_SELECTION_DIRECTION : FORWARD_SELECTION_DIRECTION;271}272export function selectByNodesAndOffsets (startPos, endPos, needFocus) {273 var startNode = startPos.node;274 var endNode = endPos.node;275 var startNodeLength = startNode.nodeValue ? startNode.length : 0;276 var endNodeLength = endNode.nodeValue ? endNode.length : 0;277 var startOffset = Math.min(startNodeLength, startPos.offset);278 var endOffset = Math.min(endNodeLength, endPos.offset);279 var parentElement = contentEditable.findContentEditableParent(startNode);280 var inverse = isInverseSelectionContentEditable(parentElement, startPos, endPos);281 var selection = getSelectionByElement(parentElement);282 var curDocument = domUtils.findDocument(parentElement);283 var range = curDocument.createRange();284 var selectionSetter = function () {285 selection.removeAllRanges();286 //NOTE: For IE we can't create inverse selection287 if (!inverse) {288 range.setStart(startNode, startOffset);289 range.setEnd(endNode, endOffset);290 selection.addRange(range);291 }292 else if (browserUtils.isIE) {293 range.setStart(endNode, endOffset);294 range.setEnd(startNode, startOffset);295 selection.addRange(range);296 }297 else {298 range.setStart(startNode, startOffset);299 range.setEnd(startNode, startOffset);300 selection.addRange(range);301 var shouldCutEndOffset = browserUtils.isSafari || browserUtils.isChrome && browserUtils.version < 58;302 var extendSelection = (node, offset) => {303 // NODE: in some cases in Firefox extend method raises error so we use try-catch304 try {305 selection.extend(node, offset);306 }307 catch (err) {308 return false;309 }310 return true;311 };312 if (shouldCutEndOffset && contentEditable.isInvisibleTextNode(endNode)) {313 if (!extendSelection(endNode, Math.min(endOffset, 1)))314 extendSelection(endNode, 0);315 }316 else317 extendSelection(endNode, endOffset);318 }319 };320 selectionSandbox.wrapSetterSelection(parentElement, selectionSetter, needFocus, true);321}322function deleteSelectionRanges (el) {323 var selection = getSelectionByElement(el);324 var rangeCount = selection.rangeCount;325 if (!rangeCount)326 return;327 for (var i = 0; i < rangeCount; i++)328 selection.getRangeAt(i).deleteContents();329}330export function deleteSelectionContents (el, selectAll) {331 var startSelection = getSelectionStart(el);332 var endSelection = getSelectionEnd(el);333 if (selectAll)334 selectContentEditable(el);335 if (startSelection === endSelection)336 return;337 // NOTE: If selection is not contain initial and final invisible symbols338 //we should select its339 correctContentEditableSelectionBeforeDelete(el);340 deleteSelectionRanges(el);341 var selection = getSelectionByElement(el);342 var range = null;343 //NOTE: We should try to do selection collapsed344 if (selection.rangeCount && !selection.getRangeAt(0).collapsed) {345 range = selection.getRangeAt(0);346 range.collapse(true);347 }348}349export function setCursorToLastVisiblePosition (el) {350 var position = contentEditable.getLastVisiblePosition(el);351 selectContentEditable(el, position, position);352}353export function hasElementContainsSelection (el) {354 var selection = getSelectionByElement(el);355 return selection.anchorNode && selection.focusNode ?356 domUtils.isElementContainsNode(el, selection.anchorNode) &&357 domUtils.isElementContainsNode(el, selection.focusNode) :358 false;...
newPoint.backend.js
Source:newPoint.backend.js
...31 var tmp = jQuery( response.data.view );32 tmp.css({display: 'none'});33 jQuery( '.table-type-project[data-id=' + response.data.task_id + ']' ).after( tmp );34 tmp.slideDown(400);35 window.eoxiaJS.taskManager.core.selectContentEditable( tmp.find( '.task-title' ) );36 count_uncompleted_tasks = parseInt ( element.find( '.count-uncompleted-tasks' ).text() );37 count_uncompleted_tasks = count_uncompleted_tasks + 1 ;38 element.find( '.count-uncompleted-tasks' ).html( count_uncompleted_tasks );39 }40 window.eoxiaJS.taskManager.newTask.stickyAction();41};42window.eoxiaJS.taskManager.newPoint.editTitle = function() {43 var data = {};44 var element;45 if ( ! element ) {46 element = jQuery( this );47 }48 data.action = 'edit_point';49 data._wpnonce = element.closest( '.table-row' ).data( 'nonce' );50 data.id = element.closest( '.table-row' ).data( 'id' );51 data.parent_id = element.closest( '.table-row' ).data( 'post-id' );52 data.content = element.html();53 data.notif = window.eoxiaJS.taskManager.comment.searchFollowerInContentEditable( element );54 window.eoxiaJS.loader.display( element.closest( 'div' ) );55 window.eoxiaJS.request.send( element, data );56};57window.eoxiaJS.taskManager.newPoint.editCreatedDate = function() {58 var data = {};59 var element;60 if ( ! element ) {61 element = jQuery( this );62 }63 data.action = 'edit_created_date';64 data.id = element.closest( '.table-row' ).data( 'id' );65 data.created_date = element.closest( '.table-row' ).find( '.mysql-date' ).val();66 window.eoxiaJS.loader.display( element.closest( 'div' ) );67 window.eoxiaJS.request.send( element, data );68};69/**70 * Envoie une requête pour passer le point en compléter ou décompléter.71 * Déplace le point vers la liste à puce "compléter" ou "décompléter".72 *73 * @since 1.0.074 */75window.eoxiaJS.taskManager.newPoint.completePoint = function( event ) {76 const data = {77 action: 'complete_point',78 _wpnonce: jQuery( this ).closest( '.table-row' ).data('nonce' ),79 parent_id: jQuery( this ).closest( '.table-row' ).data( 'post-id' ),80 id: jQuery( this ).closest( '.table-row' ).data( 'id' ),81 complete: jQuery( this ).is( ':checked' )82 };83 window.eoxiaJS.request.send( jQuery( this ), data, function( triggeredElement, response ) {84 var tableRow = triggeredElement.closest( '.table-row' );85 var projectID = tableRow.attr( 'data-post-id' );86 var element = jQuery( '.table-projects .table-row[data-id=' + projectID + '] .cell-project-status .table-cell-container' );87 if ( response.success ) {88 if ( response.data.completed ) {89 tableRow.addClass( 'task-completed' );90 if (jQuery( '.table-projects .table-row[data-id=' + projectID + '] .load-complete-point.active[data-point-state=completed]' ).length > 0) {91 jQuery( '.table-projects .table-row[data-post-id=' + projectID + ']:last' ).after( tableRow );92 } else {93 count_uncompleted_tasks = element.find( '.count-uncompleted-tasks' ).text();94 count_completed_tasks = element.find( '.count-completed-tasks' ).text();95 count_uncompleted_tasks = count_uncompleted_tasks - 1;96 count_completed_tasks = parseInt ( count_completed_tasks ) + 1;97 element.find( '.count-uncompleted-tasks' ).html( count_uncompleted_tasks );98 element.find( '.count-completed-tasks' ).text( count_completed_tasks );99 tableRow.fadeOut();100 }101 } else {102 count_uncompleted_tasks = parseInt ( element.find( '.count-uncompleted-tasks' ).text());103 count_completed_tasks = element.find( '.count-completed-tasks' ).text();104 count_uncompleted_tasks = parseInt ( count_uncompleted_tasks + 1 );105 count_completed_tasks = count_completed_tasks - 1;106 element.find( '.count-uncompleted-tasks' ).html( count_uncompleted_tasks );107 element.find( '.count-completed-tasks' ).text( count_completed_tasks );108 tableRow.removeClass( 'task-completed' );109 }110 }111 } );112};113window.eoxiaJS.taskManager.newPoint.toggleComments = function() {114 const taskID = jQuery( this ).closest( '.table-row' ).data( 'id' );115 if ( jQuery( this ).find( '.fas' ).hasClass( 'fa-angle-down' ) ) {116 jQuery( this ).find( '.fas' ).removeClass( 'fa-angle-down' ).addClass( 'fa-angle-right' );117 jQuery( '.table-type-comment[data-parent-id=' + taskID + ']' ).slideUp(400, function() {118 jQuery( this ).remove();119 });120 jQuery( '.table-type-task[data-id=' + taskID + '] .task-add div[data-action="edit_comment"]' ).attr( 'data-toggle', false );121 } else {122 var data = {};123 var element;124 if ( ! element ) {125 element = jQuery( this );126 }127 data.action = 'load_comments';128 data._wpnonce = element.closest( '.table-row' ).data( 'nonce' );129 data.id = element.closest( '.table-row' ).data( 'id' );130 data.parent_id = element.closest( '.table-row' ).data('post-id');131 window.eoxiaJS.loader.display( element );132 window.eoxiaJS.request.send( element, data );133 jQuery( this ).find( '.fas' ).removeClass( 'fa-angle-right' ).addClass( 'fa-angle-down' );134 jQuery( '.table-type-task[data-id=' + taskID + '] .task-add div[data-action="edit_comment"]' ).attr( 'data-toggle', true );135 }136};137/**138 * Le callback en cas de réussite à la requête Ajax "load_point".139 * Met le contenu dans la div.point.140 *141 * @param {HTMLDivElement} triggeredElement L'élement HTML déclenchant la requête Ajax.142 * @param {Object} response Les données renvoyées par la requête Ajax.143 * @return {void}144 *145 * @since 1.0.0.0146 * @version 1.0.0.0147 */148window.eoxiaJS.taskManager.newPoint.loadedPointSuccess = function( triggeredElement, response ) {149 var view = jQuery( response.data.view );150 view.css({display: 'none'});151 var row = triggeredElement.closest( '.table-row' );152 row.after(view);153 view.slideDown(400);154 window.eoxiaJS.taskManager.newTask.stickyAction();155 triggeredElement.removeClass( 'loading' );156 if ( triggeredElement.hasClass( 'action-attribute' ) ) {157 triggeredElement.attr( 'data-toggle', true );158 }159 if (triggeredElement.hasClass( 'cell-toggle' ) ) {160 triggeredElement.closest( '.table-row' ).find( '.load-complete-point:not(.active)[data-point-state="uncompleted"]' ).addClass( 'active' );161 }162 if ( 'addedPointSuccess' == response.data.callback_success ) {163 window.eoxiaJS.taskManager.core.selectContentEditable( jQuery( '.table-type-task[data-id=' + response.data.point.data.id + '] .task-title' ) );164 }...
Label.js
Source:Label.js
...38 autocompleteOpen && (Array.isArray(validSuggestions) ? validSuggestions : Object.keys(validSuggestions)).length > 0;39 useEffect(() => {40 if (editing) {41 setContent(option.email);42 selectContentEditable(ref.current);43 }44 }, [editing, option]);45 const onTagClick = useCallback(46 (event) => {47 onClick && onClick(index);48 event.stopPropagation();49 event.preventDefault();50 },51 [index, onClick],52 );53 const onTagFocus = useCallback(() => {54 onFocus && onFocus(index);55 }, [index, onFocus]);56 const onTagRemove = useCallback(...
newComment.backend.js
Source:newComment.backend.js
...40 var tmp = jQuery( response.data.view );41 tmp.css({display: 'none'});42 jQuery( '.table-type-task[data-id=' + response.data.point.data.id + ']' ).after( tmp );43 tmp.slideDown(400);44 window.eoxiaJS.taskManager.core.selectContentEditable( tmp.find( '.comment-title' ) );45 }46 triggeredElement.closest( '.row-empty' ).remove();47 const comment = response.data.comment;48 jQuery( '.table-type-project[data-id=' + comment.data.post_id + '] .project-time .elapsed' ).text( response.data.time.task );49 jQuery( '.table-type-task[data-id=' + comment.data.parent_id + '] .task-time .elapsed' ).text( response.data.time.point );50 jQuery( '.table-type-task[data-id=' + comment.data.parent_id + '] .number-comments' ).text( response.data.point.data.count_comments );51 window.eoxiaJS.taskManager.newTask.stickyAction();52};53window.eoxiaJS.taskManager.newComment.editedCommentSuccess = function( triggeredElement, response ) {54 const comment = response.data.comment;55 jQuery( '.table-type-project[data-id=' + comment.data.post_id + '] .project-time .elapsed' ).text( response.data.time.task );56 jQuery( '.table-type-task[data-id=' + comment.data.parent_id + '] .task-time .elapsed' ).text( response.data.time.point );57};58/**59 * Le callback en cas de réussite à la requête Ajax "load_comments".60 * Met le contenu dans la div.comments.61 *62 * @param {HTMLDivElement} triggeredElement L'élement HTML déclenchant la requête Ajax.63 * @param {Object} response Les données renvoyées par la requête Ajax.64 * @return {void}65 *66 * @since 1.0.0.067 * @version 1.0.0.068 */69window.eoxiaJS.taskManager.newComment.loadedCommentsSuccess = function( triggeredElement, response ) {70 var view = jQuery( response.data.view );71 view.css({display: 'none'});72 var row = triggeredElement.closest( '.table-row' );73 row.after(view);74 view.slideDown( 400 );75 triggeredElement.removeClass( 'loading' );76 if ( triggeredElement.hasClass( 'action-attribute' ) ) {77 triggeredElement.attr( 'data-toggle', true );78 }79 if ( 'addedCommentSuccess' == response.data.callback_success ) {80 window.eoxiaJS.taskManager.core.selectContentEditable( jQuery( '.table-type-comment[data-id=' + response.data.comment.data.id + '] .comment-title' ) );81 }82 window.eoxiaJS.taskManager.newTask.stickyAction();...
core.backend.js
Source:core.backend.js
1/**2 * Initialise l'objet "core" ainsi que la méthode "init" obligatoire pour la bibliothèque EoxiaJS.3 *4 * @since 1.0.05 * @version 1.0.06 */78window.eoxiaJS.taskManager.core = {};910/**11 * La méthode appelée automatiquement par la bibliothèque EoxiaJS.12 *13 * @return {void}14 *15 * @since 1.0.016 * @version 1.0.017 */18window.eoxiaJS.taskManager.core.init = function() {19 window.eoxiaJS.taskManager.core.event();20};2122/**23 * La méthode contenant tous les évènements pour la core.24 *25 * @since 1.0.026 * @version 1.0.027 *28 * @return {void}29 */30window.eoxiaJS.taskManager.core.event = function() {31 var action = {32 action: 'tm_have_patch_note',33 };3435 jQuery.post( ajaxurl, action, function ( response ) {36 if ( response.data.status ) {37 jQuery( '.tm-wrap' ).append( response.data.view );38 }39 } );4041 jQuery( document ).on( 'click', '.tm-wrap .wpeo-notification.patch-note.notification-active', window.eoxiaJS.taskManager.core.openPopup );42 jQuery( document ).on( 'click', '.tm-wrap .wpeo-notification.patch-note .notification-close', window.eoxiaJS.taskManager.core.closeNotification );43};4445/**46 * Ajoutes la classe 'active' dans l'élement 'popup.path-note'.47 *48 * @since 1.0.049 * @version 1.0.050 *51 * @param {MouseEvent} event Les attributs de l'évènement.52 * @return {void}53 */54window.eoxiaJS.taskManager.core.openPopup = function( event ) {55 event.stopPropagation();56 jQuery( '.tm-wrap .wpeo-modal.patch-note' ).addClass( 'modal-active' );57};5859/**60 * Ajoutes la classe 'active' dans l'élement 'popup.path-note'.61 *62 * @since 1.0.063 * @version 1.0.064 *65 * @param {MouseEvent} event Les attributs de l'évènement.66 * @return {void}67 */68window.eoxiaJS.taskManager.core.closeNotification = function( event ) {69 event.stopPropagation();70 jQuery( this ).closest( '.wpeo-notification' ).removeClass( 'notification-active' );71};7273/**74 * Actives ou désactive l'évènement unload pour le "safeExit".75 *76 * @since 1.6.077 * @version 1.6.078 *79 * @param {boolean} add True active, false désactive l'évènement.80 * @return {void}81 */82window.eoxiaJS.taskManager.core.initSafeExit = function( add ) {83 if ( add ) {84 window.addEventListener( 'beforeunload', window.eoxiaJS.taskManager.core.safeExit );85 } else {86 window.removeEventListener( 'beforeunload', window.eoxiaJS.taskManager.core.safeExit );8788 }89}90/**91 * Ajoutes une popup si l'utilisateur essai de quitter la page.92 *93 * @since 1.6.094 * @version 1.6.095 *96 * @return {void}97 */98window.eoxiaJS.taskManager.core.safeExit = function() {99 var confirmationMessage = 'The changes you have made will not be saved.';100101 event.returnValue = confirmationMessage;102 return confirmationMessage;103}104105window.eoxiaJS.taskManager.core.selectContentEditable = function( cell ) {106 cell = cell[0] ? cell[0] : cell;107 // select all text in contenteditable108 // see http://stackoverflow.com/a/6150060/145346109 var range, selection;110 if (document.body.createTextRange) {111 range = document.body.createTextRange();112 range.moveToElementText(cell);113 range.select();114 } else if (window.getSelection) {115 selection = window.getSelection();116 range = document.createRange();117 range.selectNodeContents(cell);118 selection.removeAllRanges();119 selection.addRange(range);120 }
...
utils.js
Source:utils.js
1export function selectContentEditable(element) {2 element.focus();3 // Move the cursor to the end.4 const selection = window.getSelection();5 if (selection) {6 selection.removeAllRanges();7 const range = document.createRange();8 range.selectNodeContents(element);9 range.collapse(false);10 selection.addRange(range);11 }12}13function filterArraySuggestions(filter, suggestions) {14 const lowerFilter = filter.toLowerCase();15 return suggestions.filter(...
Using AI Code Generation
1import { Selector } from 'testcafe';2test('My first test', async t => {3 .typeText('#developer-name', 'John Smith')4 .click('#windows')5 .click('#submit-button')6 .wait(1000);7 await t.selectContentEditable('#article-content', {startNode: '#article-content', startOffset: 0, endNode: '#article-content', endOffset: 0});8});9await t.selectContentEditable('#article-content', {startNode: '#article-content', startOffset: 0, endNode: '#article-content', endOffset: 0});10await t.selectContentEditable('#article-content', {startNode: '#article-content', startOffset: 0, endNode: '#article-content', endOffset: 0});11await t.selectContentEditable('#article-content', {startNode: '#article-content', startOffset: 0, endNode: '#article-content', endOffset: 0});12await t.selectContentEditable('#article-content',
Using AI Code Generation
1import { Selector } from 'testcafe';2test('My first test', async t => {3 .typeText('#developer-name', 'John Smith')4 .click('#submit-button')5 .wait(5000);6});7test('Content Editable', async t => {8 .selectContentEditable('#contentEditable', 0, 0)9 .pressKey('backspace')10 .typeText('#contentEditable', 'Hello, World!')11 .wait(5000);12});13const text = Selector('#contentEditable').innerText;14 .selectText(text, 0, 0)15 .pressKey('backspace')16 .typeText(text, 'Hello, World!')17 .wait(5000);18const text = Selector('#contentEditable').innerText;19 .selectText(text, 0, 0)20 .pressKey('backspace')21 .typeText('#contentEditable', 'Hello, World!')22 .wait(5000);23const text = Selector('#contentEditable').innerText;24 .selectText(text, 0, 0)25 .pressKey('backspace')26 .typeText(text, 'Hello, World!')27 .wait(5000);28const text = Selector('#contentEditable').innerText;29 .selectText(text, 0, 0)30 .pressKey('backspace')31 .typeText(text, 'Hello, World!')32 .wait(5000);33const text = Selector('#contentEditable').innerText;34 .selectText(text, 0, 0)35 .pressKey('backspace')36 .typeText(text, 'Hello, World!')37 .wait(5000);
Using AI Code Generation
1import { Selector } from 'testcafe';2test('My first test', async t => {3 .selectContentEditable('This is a sample contenteditable element')4 .typeText('input[type=tel]', '1234567890')5 .click('#tried-test-cafe');6});7import { Selector } from 'testcafe';8test('My first test', async t => {9 .selectContentEditable('This is a sample contenteditable element')10 .typeText('input[type=tel]', '1234567890')11 .click('#tried-test-cafe');12});13import { Selector } from 'testcafe';14test('My first test', async t => {15 .selectContentEditable('This is a sample contenteditable element')16 .typeText('input[type=tel]', '1234567890')17 .click('#tried-test-cafe');18});19import { Selector } from 'testcafe';20test('My first test', async t => {21 .selectContentEditable('This is a sample contenteditable element')22 .typeText('input[type=tel]', '1234567890')23 .click('#tried-test-cafe');24});25import { Selector } from 'testcafe';
Using AI Code Generation
1import { Selector } from 'testcafe';2test('My first test', async t => {3 .selectText(Selector('#developer-name'))4 .pressKey('delete');5});6import { Selector } from 'testcafe';7test('My first test', async t => {8 .selectContentEditable(Selector('#developer-name'))9 .pressKey('delete');10});11import { Selector } from 'testcafe';12test('My first test', async t => {13 .selectTextAreaContent(Selector('#developer-name'))14 .pressKey('delete');15});16import { Selector } from 'testcafe';17test('My first test', async t => {18 .selectEditableContent(Selector('#developer-name'))19 .pressKey('delete');20});21import { Selector } from 'testcafe';22test('My first test', async t => {23 .select(Selector('#developer-name'))24 .pressKey('delete');25});26import { Selector } from 'testcafe';27test('My first test', async t => {28 .select(Selector('#developer-name'))29 .pressKey('delete');30});31import { Selector } from 'testcafe';32test('My first test', async t => {33 .selectEditableContent(Selector
Using AI Code Generation
1import { Selector } from 'testcafe';2test('My test', async t => {3 .click(Selector('#editor'))4 .selectContentEditable('#editor', 'This is the new text');5});6import { Selector } from 'testcafe';7test('My test', async t => {8 .click(Selector('#editor'))9 .selectEditableContent('#editor', 'This is the new text');10});
Using AI Code Generation
1import { Selector } from 'testcafe';2import { selectContentEditable } from 'testcafe-selection-caret';3test('My test', async t => {4 const contentEditable = Selector('div[contenteditable=true]');5 .click(contentEditable)6 .typeText(contentEditable, 'Hello')7 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(5)8 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(5)9 .pressKey('backspace')10 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(4)11 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(4)12 .pressKey('backspace')13 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(3)14 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(3)15 .pressKey('backspace')16 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(2)17 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(2)18 .pressKey('backspace')19 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(1)20 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(1)21 .pressKey('backspace')22 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(0)23 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionEnd()).eql(0)24 .pressKey('backspace')25 .expect(selectContentEditable(contentEditable).with({ boundTestRun: t }).getSelectionStart()).eql(0)
Using AI Code Generation
1import { Selector } from 'testcafe';2test('TestCafe', async t => {3 await t.selectContentEditable(Selector('div'), 'test');4});5import { Selector } from 'testcafe';6test('TestCafe', async t => {7 await t.selectEditableContent(Selector('div'), 'test');8});9import { Selector } from 'testcafe';10test('TestCafe', async t => {11 await t.selectEditableContent(Selector('div'), 'test');12});13import { Selector } from 'testcafe';14test('TestCafe', async t => {15 await t.selectEditableContent(Selector('div'), 'test');16});17import { Selector } from 'testcafe';18test('TestCafe', async t => {19 await t.selectEditableContent(Selector('div'), 'test');20});21import { Selector } from 'testcafe';22test('TestCafe', async t => {23 await t.selectEditableContent(Selector('div'), 'test');24});25import { Selector } from 'testcafe';26test('TestCafe', async t => {27 await t.selectEditableContent(Selector('div'), 'test');28});29import { Selector } from 'testcafe';30test('TestCafe', async t => {
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!