diff --git a/app.py b/app.py new file mode 100644 index 000000000..f12908578 --- /dev/null +++ b/app.py @@ -0,0 +1,113 @@ +import sqlite3 +import base64 +from flask import Flask, request, send_from_directory +from flask_restful import Resource, Api +from flask_cors import CORS + +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + if isinstance(row[idx], buffer): + d[col[0]] = base64.b64encode(row[idx]) + else: + d[col[0]] = row[idx] + + return d + +conn = sqlite3.connect('demo.ncdb') +conn.row_factory = dict_factory + +app = Flask(__name__) + +CORS(app) + +@app.route('/frontend/') +def send_js(path): + return send_from_directory('frontend', path) + +api = Api(app) + +def insert(tablename, rec): + keys = ','.join(rec.keys()) + question_marks = ','.join(list('?'*len(rec))) + values = tuple(rec.values()) + execute('INSERT INTO '+tablename+' ('+keys+') VALUES ('+question_marks+')', values) + +def delete(tablename, note_id): + execute("DELETE FROM " + tablename + " WHERE note_id = ?", [note_id]) + +def execute(sql, params=[]): + cursor = conn.cursor() + cursor.execute(sql, params) + +def getResults(sql, params=[]): + cursor = conn.cursor() + query = cursor.execute(sql, params) + return query.fetchall() + +def getSingleResult(sql, params=()): + cursor = conn.cursor() + query = cursor.execute(sql, params) + return query.fetchone() + +class Query(Resource): + def get(self): + sql = request.args.get('sql') + + return getResults(sql) + +api.add_resource(Query, '/query') + +class Notes(Resource): + def get(self, note_id): + return { + 'detail': getSingleResult("select * from notes where note_id = ?", [note_id]), + 'formatting': getResults("select * from formatting where note_id = ? order by note_offset", [note_id]), + 'links': getResults("select * from links where note_id = ? order by note_offset", [note_id]), + 'images': getResults("select * from images where note_id = ? order by note_offset", [note_id]) + } + + def put(self, note_id): + note = request.get_json(force=True) + + execute("update notes set note_text = ?, note_title = ? where note_id = ?", [note['detail']['note_text'], note['detail']['note_title'], note_id]) + + delete("formatting", note_id) + + for fmt in note['formatting']: + insert("formatting", fmt) + + conn.commit() + + return {} + +api.add_resource(Notes, '/notes/') + +class Tree(Resource): + def get(self): + notes = getResults("select notes_tree.*, notes.note_title from notes_tree join notes on notes.note_id = notes_tree.note_id order by note_pid, note_pos") + + rootNotes = [] + notesMap = {} + + for note in notes: + note['children'] = [] + + if not note['note_pid']: + rootNotes.append(note) + + notesMap[note['note_id']] = note + + for note in notes: + if note['note_pid'] != "": + parent = notesMap[note['note_pid']] + + parent['children'].append(note) + parent['folder'] = True + + return rootNotes + +api.add_resource(Tree, '/tree') + +if __name__ == '__main__': + app.run(host='0.0.0.0') diff --git a/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 000000000..63118419b --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": [ + ["latest", { + "es2015": { "modules": false } + }] + ] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..c369a4d90 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log +yarn-error.log diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..b6434b2c6 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,18 @@ +# frontend + +> A Vue.js project + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build +``` + +For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..c86ce0622 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,286 @@ + + + + + frontend + + + +
+   + +
+ +
+
+ +
+ +
+
+ +
+ +
+ Nothing here right now! +
+
+
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/js/fancytree/LICENSE.txt b/frontend/js/fancytree/LICENSE.txt new file mode 100644 index 000000000..980b1b245 --- /dev/null +++ b/frontend/js/fancytree/LICENSE.txt @@ -0,0 +1,21 @@ +Copyright 2008-2017 Martin Wendt, +http://wwWendt.de/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/frontend/js/fancytree/jquery.fancytree-all.js b/frontend/js/fancytree/jquery.fancytree-all.js new file mode 100644 index 000000000..564074bc6 --- /dev/null +++ b/frontend/js/fancytree/jquery.fancytree-all.js @@ -0,0 +1,9108 @@ +/*! + * jquery.fancytree.js + * Tree view control with support for lazy loading and much more. + * https://github.com/mar10/fancytree/ + * + * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de) + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.22.5 + * @date 2017-05-11T17:01:53Z + */ + +/** Core Fancytree module. + */ + + +// Start of local namespace +;(function($, window, document, undefined) { +"use strict"; + +// prevent duplicate loading +if ( $.ui && $.ui.fancytree ) { + $.ui.fancytree.warn("Fancytree: ignored duplicate include"); + return; +} + + +/* ***************************************************************************** + * Private functions and variables + */ + +var i, attr, + FT = null, // initialized below + TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/' + REX_HTML = /[&<>"'\/]/g, + REX_TOOLTIP = /[<>"'\/]/g, + RECURSIVE_REQUEST_ERROR = "$recursive_request", + ENTITY_MAP = {"&": "&", "<": "<", ">": ">", "\"": """, "'": "'", "/": "/"}, + IGNORE_KEYCODES = { 16: true, 17: true, 18: true }, + SPECIAL_KEYCODES = { + 8: "backspace", 9: "tab", 10: "return", 13: "return", + // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt + 19: "pause", 20: "capslock", 27: "esc", 32: "space", 33: "pageup", + 34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up", + 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", + 103: "7", 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", + 111: "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", + 117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11", + 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", + 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'"}, + MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" }, + // Boolean attributes that can be set with equivalent class names in the LI tags + CLASS_ATTRS = "active expanded focus folder hideCheckbox lazy selected unselectable".split(" "), + CLASS_ATTR_MAP = {}, + // Top-level Fancytree node attributes, that can be set by dict + NODE_ATTRS = "expanded extraClasses folder hideCheckbox icon key lazy refKey selected statusNodeType title tooltip unselectable".split(" "), + NODE_ATTR_MAP = {}, + // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase) + NODE_ATTR_LOWERCASE_MAP = {}, + // Attribute names that should NOT be added to node.data + NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true}; + +for(i=0; i t ); + } + } + return true; +} + +/** Return a wrapper that calls sub.methodName() and exposes + * this : tree + * this._local : tree.ext.EXTNAME + * this._super : base.methodName.call() + * this._superApply : base.methodName.apply() + */ +function _makeVirtualFunction(methodName, tree, base, extension, extName){ + // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName); + // if(rexTestSuper && !rexTestSuper.test(func)){ + // // extension.methodName() doesn't call _super(), so no wrapper required + // return func; + // } + // Use an immediate function as closure + var proxy = (function(){ + var prevFunc = tree[methodName], // org. tree method or prev. proxy + baseFunc = extension[methodName], // + _local = tree.ext[extName], + _super = function(){ + return prevFunc.apply(tree, arguments); + }, + _superApply = function(args){ + return prevFunc.apply(tree, args); + }; + + // Return the wrapper function + return function(){ + var prevLocal = tree._local, + prevSuper = tree._super, + prevSuperApply = tree._superApply; + + try{ + tree._local = _local; + tree._super = _super; + tree._superApply = _superApply; + return baseFunc.apply(tree, arguments); + }finally{ + tree._local = prevLocal; + tree._super = prevSuper; + tree._superApply = prevSuperApply; + } + }; + })(); // end of Immediate Function + return proxy; +} + +/** + * Subclass `base` by creating proxy functions + */ +function _subclassObject(tree, base, extension, extName){ + // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName); + for(var attrName in extension){ + if(typeof extension[attrName] === "function"){ + if(typeof tree[attrName] === "function"){ + // override existing method + tree[attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName); + }else if(attrName.charAt(0) === "_"){ + // Create private methods in tree.ext.EXTENSION namespace + tree.ext[extName][attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName); + }else{ + $.error("Could not override tree." + attrName + ". Use prefix '_' to create tree." + extName + "._" + attrName); + } + }else{ + // Create member variables in tree.ext.EXTENSION namespace + if(attrName !== "options"){ + tree.ext[extName][attrName] = extension[attrName]; + } + } + } +} + + +function _getResolvedPromise(context, argArray){ + if(context === undefined){ + return $.Deferred(function(){this.resolve();}).promise(); + }else{ + return $.Deferred(function(){this.resolveWith(context, argArray);}).promise(); + } +} + + +function _getRejectedPromise(context, argArray){ + if(context === undefined){ + return $.Deferred(function(){this.reject();}).promise(); + }else{ + return $.Deferred(function(){this.rejectWith(context, argArray);}).promise(); + } +} + + +function _makeResolveFunc(deferred, context){ + return function(){ + deferred.resolveWith(context); + }; +} + + +function _getElementDataAsDict($el){ + // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. + var d = $.extend({}, $el.data()), + json = d.json; + + delete d.fancytree; // added to container by widget factory (old jQuery UI) + delete d.uiFancytree; // added to container by widget factory + + if( json ) { + delete d.json; + //
  • is already returned as object (http://api.jquery.com/data/#data-html5) + d = $.extend(d, json); + } + return d; +} + + +function _escapeHtml(s){ + return ("" + s).replace(REX_HTML, function(s) { + return ENTITY_MAP[s]; + }); +} + + +function _escapeTooltip(s){ + return ("" + s).replace(REX_TOOLTIP, function(s) { + return ENTITY_MAP[s]; + }); +} + + +// TODO: use currying +function _makeNodeTitleMatcher(s){ + s = s.toLowerCase(); + return function(node){ + return node.title.toLowerCase().indexOf(s) >= 0; + }; +} + + +function _makeNodeTitleStartMatcher(s){ + var reMatch = new RegExp("^" + s, "i"); + return function(node){ + return reMatch.test(node.title); + }; +} + + +/* ***************************************************************************** + * FancytreeNode + */ + + +/** + * Creates a new FancytreeNode + * + * @class FancytreeNode + * @classdesc A FancytreeNode represents the hierarchical data model and operations. + * + * @param {FancytreeNode} parent + * @param {NodeData} obj + * + * @property {Fancytree} tree The tree instance + * @property {FancytreeNode} parent The parent node + * @property {string} key Node id (must be unique inside the tree) + * @property {string} title Display name (may contain HTML) + * @property {object} data Contains all extra data that was passed on node creation + * @property {FancytreeNode[] | null | undefined} children Array of child nodes.
    + * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array + * to define a node that has no children. + * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. + * @property {string} extraClasses Additional CSS classes, added to the node's `<span>`.
    + * Note: use `node.add/remove/toggleClass()` to modify. + * @property {boolean} folder Folder nodes have different default icons and click behavior.
    + * Note: Also non-folders may have children. + * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'. + * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. + * @property {boolean} selected Use isSelected(), setSelected() to access this property. + * @property {string} tooltip Alternative description used as hover popup + */ +function FancytreeNode(parent, obj){ + var i, l, name, cl; + + this.parent = parent; + this.tree = parent.tree; + this.ul = null; + this.li = null; //
  • tag + this.statusNodeType = null; // if this is a temp. node to display the status of its parent + this._isLoading = false; // if this node itself is loading + this._error = null; // {message: '...'} if a load error occurred + this.data = {}; + + // TODO: merge this code with node.toDict() + // copy attributes from obj object + for(i=0, l=NODE_ATTRS.length; i= 0, "insertBefore must be an existing child"); + // insert nodeList after children[pos] + this.children.splice.apply(this.children, [pos, 0].concat(nodeList)); + } + if ( origFirstChild && !insertBefore ) { + // #708: Fast path -- don't render every child of root, just the new ones! + // #723, #729: but only if it's appended to an existing child list + for(i=0, l=nodeList.length; i= 0; i--) { + n = this.children[i]; + if( n.statusNodeType === "paging" ) { + this.removeChild(n); + } + } + this.partload = false; + return; + } + node = $.extend({ + title: this.tree.options.strings.moreData, + statusNodeType: "paging", + icon: false + }, node); + this.partload = true; + return this.addNode(node, mode); + }, + /** + * Append new node after this. + * + * This a convenience function that calls addNode(node, 'after') + * + * @param {NodeData} node node definition + * @returns {FancytreeNode} new node + */ + appendSibling: function(node){ + return this.addNode(node, "after"); + }, + /** + * Modify existing child nodes. + * + * @param {NodePatch} patch + * @returns {$.Promise} + * @see FancytreeNode#addChildren + */ + applyPatch: function(patch) { + // patch [key, null] means 'remove' + if(patch === null){ + this.remove(); + return _getResolvedPromise(this); + } + // TODO: make sure that root node is not collapsed or modified + // copy (most) attributes to node.ATTR or node.data.ATTR + var name, promise, v, + IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global + + for(name in patch){ + v = patch[name]; + if( !IGNORE_MAP[name] && !$.isFunction(v)){ + if(NODE_ATTR_MAP[name]){ + this[name] = v; + }else{ + this.data[name] = v; + } + } + } + // Remove and/or create children + if(patch.hasOwnProperty("children")){ + this.removeChildren(); + if(patch.children){ // only if not null and not empty list + // TODO: addChildren instead? + this._setChildren(patch.children); + } + // TODO: how can we APPEND or INSERT child nodes? + } + if(this.isVisible()){ + this.renderTitle(); + this.renderStatus(); + } + // Expand collapse (final step, since this may be async) + if(patch.hasOwnProperty("expanded")){ + promise = this.setExpanded(patch.expanded); + }else{ + promise = _getResolvedPromise(this); + } + return promise; + }, + /** Collapse all sibling nodes. + * @returns {$.Promise} + */ + collapseSiblings: function() { + return this.tree._callHook("nodeCollapseSiblings", this); + }, + /** Copy this node as sibling or child of `node`. + * + * @param {FancytreeNode} node source node + * @param {string} [mode=child] 'before' | 'after' | 'child' + * @param {Function} [map] callback function(NodeData) that could modify the new node + * @returns {FancytreeNode} new + */ + copyTo: function(node, mode, map) { + return node.addNode(this.toDict(true, map), mode); + }, + /** Count direct and indirect children. + * + * @param {boolean} [deep=true] pass 'false' to only count direct children + * @returns {int} number of child nodes + */ + countChildren: function(deep) { + var cl = this.children, i, l, n; + if( !cl ){ + return 0; + } + n = cl.length; + if(deep !== false){ + for(i=0, l=n; i= 2 (prepending node info) + * + * @param {*} msg string or object or array of such + */ + debug: function(msg){ + if( this.tree.options.debugLevel >= 2 ) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("log", arguments); + } + }, + /** Deprecated. + * @deprecated since 2014-02-16. Use resetLazy() instead. + */ + discard: function(){ + this.warn("FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."); + return this.resetLazy(); + }, + /** Remove DOM elements for all descendents. May be called on .collapse event + * to keep the DOM small. + * @param {boolean} [includeSelf=false] + */ + discardMarkup: function(includeSelf){ + var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup"; + this.tree._callHook(fn, this); + }, + /**Find all nodes that match condition (excluding self). + * + * @param {string | function(node)} match title string to search for, or a + * callback function that returns `true` if a node is matched. + * @returns {FancytreeNode[]} array of nodes (may be empty) + */ + findAll: function(match) { + match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); + var res = []; + this.visit(function(n){ + if(match(n)){ + res.push(n); + } + }); + return res; + }, + /**Find first node that matches condition (excluding self). + * + * @param {string | function(node)} match title string to search for, or a + * callback function that returns `true` if a node is matched. + * @returns {FancytreeNode} matching node or null + * @see FancytreeNode#findAll + */ + findFirst: function(match) { + match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); + var res = null; + this.visit(function(n){ + if(match(n)){ + res = n; + return false; + } + }); + return res; + }, + /* Apply selection state (internal use only) */ + _changeSelectStatusAttrs: function (state) { + var changed = false; + + switch(state){ + case false: + changed = ( this.selected || this.partsel ); + this.selected = false; + this.partsel = false; + break; + case true: + changed = ( !this.selected || !this.partsel ); + this.selected = true; + this.partsel = true; + break; + case undefined: + changed = ( this.selected || !this.partsel ); + this.selected = false; + this.partsel = true; + break; + default: + _assert(false, "invalid state: " + state); + } + // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); + if( changed ){ + this.renderStatus(); + } + return changed; + }, + /** + * Fix selection status, after this node was (de)selected in multi-hier mode. + * This includes (de)selecting all children. + */ + fixSelection3AfterClick: function() { + var flag = this.isSelected(); + +// this.debug("fixSelection3AfterClick()"); + + this.visit(function(node){ + node._changeSelectStatusAttrs(flag); + }); + this.fixSelection3FromEndNodes(); + }, + /** + * Fix selection status for multi-hier mode. + * Only end-nodes are considered to update the descendants branch and parents. + * Should be called after this node has loaded new children or after + * children have been modified using the API. + */ + fixSelection3FromEndNodes: function() { +// this.debug("fixSelection3FromEndNodes()"); + _assert(this.tree.options.selectMode === 3, "expected selectMode 3"); + + // Visit all end nodes and adjust their parent's `selected` and `partsel` + // attributes. Return selection state true, false, or undefined. + function _walk(node){ + var i, l, child, s, state, allSelected,someSelected, + children = node.children; + + if( children && children.length ){ + // check all children recursively + allSelected = true; + someSelected = false; + + for( i=0, l=children.length; i= 1 (prepending node info) + * + * @param {*} msg string or object or array of such + */ + info: function(msg){ + if( this.tree.options.debugLevel >= 1 ) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("info", arguments); + } + }, + /** Return true if node is active (see also FancytreeNode#isSelected). + * @returns {boolean} + */ + isActive: function() { + return (this.tree.activeNode === this); + }, + /** Return true if node is a direct child of otherNode. + * @param {FancytreeNode} otherNode + * @returns {boolean} + */ + isChildOf: function(otherNode) { + return (this.parent && this.parent === otherNode); + }, + /** Return true, if node is a direct or indirect sub node of otherNode. + * @param {FancytreeNode} otherNode + * @returns {boolean} + */ + isDescendantOf: function(otherNode) { + if(!otherNode || otherNode.tree !== this.tree){ + return false; + } + var p = this.parent; + while( p ) { + if( p === otherNode ){ + return true; + } + p = p.parent; + } + return false; + }, + /** Return true if node is expanded. + * @returns {boolean} + */ + isExpanded: function() { + return !!this.expanded; + }, + /** Return true if node is the first node of its parent's children. + * @returns {boolean} + */ + isFirstSibling: function() { + var p = this.parent; + return !p || p.children[0] === this; + }, + /** Return true if node is a folder, i.e. has the node.folder attribute set. + * @returns {boolean} + */ + isFolder: function() { + return !!this.folder; + }, + /** Return true if node is the last node of its parent's children. + * @returns {boolean} + */ + isLastSibling: function() { + var p = this.parent; + return !p || p.children[p.children.length-1] === this; + }, + /** Return true if node is lazy (even if data was already loaded) + * @returns {boolean} + */ + isLazy: function() { + return !!this.lazy; + }, + /** Return true if node is lazy and loaded. For non-lazy nodes always return true. + * @returns {boolean} + */ + isLoaded: function() { + return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node + }, + /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. + * @returns {boolean} + */ + isLoading: function() { + return !!this._isLoading; + }, + /* + * @deprecated since v2.4.0: Use isRootNode() instead + */ + isRoot: function() { + return this.isRootNode(); + }, + /** (experimental) Return true if this is partially loaded. + * @returns {boolean} + * @since 2.15 + */ + isPartload: function() { + return !!this.partload; + }, + /** Return true if this is the (invisible) system root node. + * @returns {boolean} + * @since 2.4 + */ + isRootNode: function() { + return (this.tree.rootNode === this); + }, + /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). + * @returns {boolean} + */ + isSelected: function() { + return !!this.selected; + }, + /** Return true if this node is a temporarily generated system node like + * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). + * @returns {boolean} + */ + isStatusNode: function() { + return !!this.statusNodeType; + }, + /** Return true if this node is a status node of type 'paging'. + * @returns {boolean} + * @since 2.15 + */ + isPagingNode: function() { + return this.statusNodeType === "paging"; + }, + /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. + * @returns {boolean} + * @since 2.4 + */ + isTopLevel: function() { + return (this.tree.rootNode === this.parent); + }, + /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. + * @returns {boolean} + */ + isUndefined: function() { + return this.hasChildren() === undefined; // also checks if the only child is a status node + }, + /** Return true if all parent nodes are expanded. Note: this does not check + * whether the node is scrolled into the visible part of the screen. + * @returns {boolean} + */ + isVisible: function() { + var i, l, + parents = this.getParentList(false, false); + + for(i=0, l=parents.length; iexpanded state is maintained. + * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded. + * @returns {$.Promise} + */ + load: function(forceReload) { + var res, source, + that = this, + wasExpanded = this.isExpanded(); + + _assert( this.isLazy(), "load() requires a lazy node" ); + // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); + if( !forceReload && !this.isUndefined() ) { + return _getResolvedPromise(this); + } + if( this.isLoaded() ){ + this.resetLazy(); // also collapses + } + // This method is also called by setExpanded() and loadKeyPath(), so we + // have to avoid recursion. + source = this.tree._triggerNodeEvent("lazyLoad", this); + if( source === false ) { // #69 + return _getResolvedPromise(this); + } + _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); + res = this.tree._callHook("nodeLoadChildren", this, source); + if( wasExpanded ) { + this.expanded = true; + res.always(function(){ + that.render(); + }); + } else { + res.always(function(){ + that.renderStatus(); // fix expander icon to 'loaded' + }); + } + return res; + }, + /** Expand all parents and optionally scroll into visible area as neccessary. + * Promise is resolved, when lazy loading and animations are done. + * @param {object} [opts] passed to `setExpanded()`. + * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} + * @returns {$.Promise} + */ + makeVisible: function(opts) { + var i, + that = this, + deferreds = [], + dfd = new $.Deferred(), + parents = this.getParentList(false, false), + len = parents.length, + effects = !(opts && opts.noAnimation === true), + scroll = !(opts && opts.scrollIntoView === false); + + // Expand bottom-up, so only the top node is animated + for(i = len - 1; i >= 0; i--){ + // that.debug("pushexpand" + parents[i]); + deferreds.push(parents[i].setExpanded(true, opts)); + } + $.when.apply($, deferreds).done(function(){ + // All expands have finished + // that.debug("expand DONE", scroll); + if( scroll ){ + that.scrollIntoView(effects).done(function(){ + // that.debug("scroll DONE"); + dfd.resolve(); + }); + } else { + dfd.resolve(); + } + }); + return dfd.promise(); + }, + /** Move this node to targetNode. + * @param {FancytreeNode} targetNode + * @param {string} mode
    +	 *      'child': append this node as last child of targetNode.
    +	 *               This is the default. To be compatble with the D'n'd
    +	 *               hitMode, we also accept 'over'.
    +	 *      'firstChild': add this node as first child of targetNode.
    +	 *      'before': add this node as sibling before targetNode.
    +	 *      'after': add this node as sibling after targetNode.
    + * @param {function} [map] optional callback(FancytreeNode) to allow modifcations + */ + moveTo: function(targetNode, mode, map) { + if(mode === undefined || mode === "over"){ + mode = "child"; + } else if ( mode === "firstChild" ) { + if( targetNode.children && targetNode.children.length ) { + mode = "before"; + targetNode = targetNode.children[0]; + } else { + mode = "child"; + } + } + var pos, + prevParent = this.parent, + targetParent = (mode === "child") ? targetNode : targetNode.parent; + + if(this === targetNode){ + return; + }else if( !this.parent ){ + $.error("Cannot move system root"); + }else if( targetParent.isDescendantOf(this) ){ + $.error("Cannot move a node to its own descendant"); + } + if( targetParent !== prevParent ) { + prevParent.triggerModifyChild("remove", this); + } + // Unlink this node from current parent + if( this.parent.children.length === 1 ) { + if( this.parent === targetParent ){ + return; // #258 + } + this.parent.children = this.parent.lazy ? [] : null; + this.parent.expanded = false; + } else { + pos = $.inArray(this, this.parent.children); + _assert(pos >= 0, "invalid source parent"); + this.parent.children.splice(pos, 1); + } + // Remove from source DOM parent +// if(this.parent.ul){ +// this.parent.ul.removeChild(this.li); +// } + + // Insert this node to target parent's child list + this.parent = targetParent; + if( targetParent.hasChildren() ) { + switch(mode) { + case "child": + // Append to existing target children + targetParent.children.push(this); + break; + case "before": + // Insert this node before target node + pos = $.inArray(targetNode, targetParent.children); + _assert(pos >= 0, "invalid target parent"); + targetParent.children.splice(pos, 0, this); + break; + case "after": + // Insert this node after target node + pos = $.inArray(targetNode, targetParent.children); + _assert(pos >= 0, "invalid target parent"); + targetParent.children.splice(pos+1, 0, this); + break; + default: + $.error("Invalid mode " + mode); + } + } else { + targetParent.children = [ this ]; + } + // Parent has no
      tag yet: +// if( !targetParent.ul ) { +// // This is the parent's first child: create UL tag +// // (Hidden, because it will be +// targetParent.ul = document.createElement("ul"); +// targetParent.ul.style.display = "none"; +// targetParent.li.appendChild(targetParent.ul); +// } +// // Issue 319: Add to target DOM parent (only if node was already rendered(expanded)) +// if(this.li){ +// targetParent.ul.appendChild(this.li); +// }^ + + // Let caller modify the nodes + if( map ){ + targetNode.visit(map, true); + } + if( targetParent === prevParent ) { + targetParent.triggerModifyChild("move", this); + } else { + // prevParent.triggerModifyChild("remove", this); + targetParent.triggerModifyChild("add", this); + } + // Handle cross-tree moves + if( this.tree !== targetNode.tree ) { + // Fix node.tree for all source nodes +// _assert(false, "Cross-tree move is not yet implemented."); + this.warn("Cross-tree moveTo is experimantal!"); + this.visit(function(n){ + // TODO: fix selection state and activation, ... + n.tree = targetNode.tree; + }, true); + } + + // A collaposed node won't re-render children, so we have to remove it manually + // if( !targetParent.expanded ){ + // prevParent.ul.removeChild(this.li); + // } + + // Update HTML markup + if( !prevParent.isDescendantOf(targetParent)) { + prevParent.render(); + } + if( !targetParent.isDescendantOf(prevParent) && targetParent !== prevParent) { + targetParent.render(); + } + // TODO: fix selection state + // TODO: fix active state + +/* + var tree = this.tree; + var opts = tree.options; + var pers = tree.persistence; + + + // Always expand, if it's below minExpandLevel +// tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel()); + if ( opts.minExpandLevel >= ftnode.getLevel() ) { +// tree.logDebug ("Force expand for %o", ftnode); + this.bExpanded = true; + } + + // In multi-hier mode, update the parents selection state + // DT issue #82: only if not initializing, because the children may not exist yet +// if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing ) +// ftnode._fixSelectionState(); + + // In multi-hier mode, update the parents selection state + if( ftnode.bSelected && opts.selectMode==3 ) { + var p = this; + while( p ) { + if( !p.hasSubSel ) + p._setSubSel(true); + p = p.parent; + } + } + // render this node and the new child + if ( tree.bEnableUpdate ) + this.render(); + + return ftnode; + +*/ + }, + /** Set focus relative to this node and optionally activate. + * + * @param {number} where The keyCode that would normally trigger this move, + * e.g. `$.ui.keyCode.LEFT` would collapse the node if it + * is expanded or move to the parent oterwise. + * @param {boolean} [activate=true] + * @returns {$.Promise} + */ + navigate: function(where, activate) { + var i, parents, res, + handled = true, + KC = $.ui.keyCode, + sib = null; + + // Navigate to node + function _goto(n){ + if( n ){ + // setFocus/setActive will scroll later (if autoScroll is specified) + try { n.makeVisible({scrollIntoView: false}); } catch(e) {} // #272 + // Node may still be hidden by a filter + if( ! $(n.span).is(":visible") ) { + n.debug("Navigate: skipping hidden node"); + n.navigate(where, activate); + return; + } + return activate === false ? n.setFocus() : n.setActive(); + } + } + + switch( where ) { + case KC.BACKSPACE: + if( this.parent && this.parent.parent ) { + res = _goto(this.parent); + } + break; + case KC.HOME: + this.tree.visit(function(n){ // goto first visible node + if( $(n.span).is(":visible") ) { + res = _goto(n); + return false; + } + }); + break; + case KC.END: + this.tree.visit(function(n){ // goto last visible node + if( $(n.span).is(":visible") ) { + res = n; + } + }); + if( res ) { + res = _goto(res); + } + break; + case KC.LEFT: + if( this.expanded ) { + this.setExpanded(false); + res = _goto(this); + } else if( this.parent && this.parent.parent ) { + res = _goto(this.parent); + } + break; + case KC.RIGHT: + if( !this.expanded && (this.children || this.lazy) ) { + this.setExpanded(); + res = _goto(this); + } else if( this.children && this.children.length ) { + res = _goto(this.children[0]); + } + break; + case KC.UP: + sib = this.getPrevSibling(); + // #359: skip hidden sibling nodes, preventing a _goto() recursion + while( sib && !$(sib.span).is(":visible") ) { + sib = sib.getPrevSibling(); + } + while( sib && sib.expanded && sib.children && sib.children.length ) { + sib = sib.children[sib.children.length - 1]; + } + if( !sib && this.parent && this.parent.parent ){ + sib = this.parent; + } + res = _goto(sib); + break; + case KC.DOWN: + if( this.expanded && this.children && this.children.length ) { + sib = this.children[0]; + } else { + parents = this.getParentList(false, true); + for(i=parents.length-1; i>=0; i--) { + sib = parents[i].getNextSibling(); + // #359: skip hidden sibling nodes, preventing a _goto() recursion + while( sib && !$(sib.span).is(":visible") ) { + sib = sib.getNextSibling(); + } + if( sib ){ break; } + } + } + res = _goto(sib); + break; + default: + handled = false; + } + return res || _getResolvedPromise(); + }, + /** + * Remove this node (not allowed for system root). + */ + remove: function() { + return this.parent.removeChild(this); + }, + /** + * Remove childNode from list of direct children. + * @param {FancytreeNode} childNode + */ + removeChild: function(childNode) { + return this.tree._callHook("nodeRemoveChild", this, childNode); + }, + /** + * Remove all child nodes and descendents. This converts the node into a leaf.
      + * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy() + * in order to trigger lazyLoad on next expand. + */ + removeChildren: function() { + return this.tree._callHook("nodeRemoveChildren", this); + }, + /** + * Remove class from node's span tag and .extraClasses. + * + * @param {string} className class name + * + * @since 2.17 + */ + removeClass: function(className){ + return this.toggleClass(className, false); + }, + /** + * This method renders and updates all HTML markup that is required + * to display this node in its current state.
      + * Note: + *
        + *
      • It should only be neccessary to call this method after the node object + * was modified by direct access to its properties, because the common + * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...) + * already handle this. + *
      • {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} + * are implied. If changes are more local, calling only renderTitle() or + * renderStatus() may be sufficient and faster. + *
      + * + * @param {boolean} [force=false] re-render, even if html markup was already created + * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed + */ + render: function(force, deep) { + return this.tree._callHook("nodeRender", this, force, deep); + }, + /** Create HTML markup for the node's outer <span> (expander, checkbox, icon, and title). + * Implies {@link FancytreeNode#renderStatus}. + * @see Fancytree_Hooks#nodeRenderTitle + */ + renderTitle: function() { + return this.tree._callHook("nodeRenderTitle", this); + }, + /** Update element's CSS classes according to node state. + * @see Fancytree_Hooks#nodeRenderStatus + */ + renderStatus: function() { + return this.tree._callHook("nodeRenderStatus", this); + }, + /** + * (experimental) Replace this node with `source`. + * (Currently only available for paging nodes.) + * @param {NodeData[]} source List of child node definitions + * @since 2.15 + */ + replaceWith: function(source) { + var res, + parent = this.parent, + pos = $.inArray(this, parent.children), + that = this; + + _assert( this.isPagingNode(), "replaceWith() currently requires a paging status node" ); + + res = this.tree._callHook("nodeLoadChildren", this, source); + res.done(function(data){ + // New nodes are currently children of `this`. + var children = that.children; + // Prepend newly loaded child nodes to `this` + // Move new children after self + for( i=0; i