├── run.sh ├── intro.gif ├── .gitignore ├── workspace.code-workspace ├── run_local.bat ├── consts.js ├── links.js ├── README.md ├── img └── r.svg ├── jquery.mousewheel.min.js ├── files.js ├── freehand.js ├── localStorage.js ├── view.js ├── jquery.ui.rotatable.min.js ├── nodes_default.js ├── search.js ├── universal.js ├── gdrive.js ├── nodes.js ├── style.css ├── index.html ├── history.js ├── places.js └── main.js /run.sh: -------------------------------------------------------------------------------- 1 | python3 -m http.server 5000 2 | -------------------------------------------------------------------------------- /intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugenpt/noteplace/HEAD/intro.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .*.swp 4 | %SystemDrive%* 5 | tags 6 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /run_local.bat: -------------------------------------------------------------------------------- 1 | 2 | @CALL "C:\ProgramData\Anaconda3\condabin\conda.bat" activate 3 | python -m http.server 5000 4 | echo bat-file-end =( 5 | -------------------------------------------------------------------------------- /consts.js: -------------------------------------------------------------------------------- 1 | // yeah, I don't like limits, but.. 2 | // without proper custom infinitely precise numbers 3 | // this is what I can do with js 4 | // 5 | // Which seems OK, 26 orders of magnitude.. 6 | // is ~ the size of observable universe in meters 7 | // 8 | // I know, I know, it would've been 9 | // way cooler if it was ~ size(universe)/size(atom nucleus) 10 | // which is.. ~ 10^26/(10^-15) ~ 10^41 11 | const zoomMax = 1e14; 12 | const zoomMin = 1e-15; 13 | 14 | const zoomK = 1.6; 15 | 16 | const container = _('#container'); 17 | const node_container = _('#node_container'); 18 | 19 | const md = new Remarkable('full', { 20 | html: true, 21 | typographer: true 22 | }); 23 | 24 | const BODY = document.getElementsByTagName('body')[0]; 25 | 26 | -------------------------------------------------------------------------------- /links.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | I propose hyper-links as natural occurence in the moteplace 5 | ( simplest example - logical AND/OR, taking 2 inputs and having 1 output) 6 | 7 | _LINKS should be saved and used later as templates 8 | 9 | Basic types should include "is part of" as a type of link. 10 | What else.. 11 | 12 | Logic? 13 | Time?? 14 | 15 | Or should I just do links as nodes? just with special properties.. 16 | 17 | Hmmmm..... 18 | 19 | What is better - special types of nodes or completely different thing? 20 | 21 | I'd say that links should be different from nodes. 22 | 23 | */ 24 | 25 | let _LINKS = []; 26 | let _LINK_TYPES = [ 27 | { 28 | name:'is part of', 29 | style:{ 30 | 31 | }, 32 | } 33 | ]; 34 | 35 | // 36 | 37 | 38 | 39 | function renderLink(link){ 40 | 41 | } 42 | 43 | // 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Noteplace 2 | 3 | at [https://eugenpt.github.io/noteplace/](https://eugenpt.github.io/noteplace/) 4 | 5 | A place, for your notes. 6 | 7 | A digital storage with characteristics of a physical space, where you can navigate using all your intuition from a physical world, but practically infinite and with search and filter (currently still on my TODO list!). 8 | 9 | ## Features: 10 | - ~Inifinite Zoom 11 | - Markdown (including URL-based images) + scale/rotate notes 12 | - Private (no server-side, only downloads required libraries and images from URLs which you provide) 13 | - Save to/Load from local file (so no internet connection really required once you download page) 14 | - Google Drive save/load (uses AppFolder, does not see any of your files) 15 | # Short demo: 16 | ![](intro.gif) 17 | 18 | --- 19 | ## Licence 20 | 21 | You may use Noteplace, and [contact me](mailto:eugen.pt@gmail.com) if you do because I'm eager to hear about it! 22 | 23 | If you wish to use the code.. well, contact me, we'll figure it out (the code is ugly, among other things I'll ask if you're really sure). 24 | 25 | Also - please don't misuse my API keys -------------------------------------------------------------------------------- /img/r.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mousewheel 3.1.13 3 | * Copyright OpenJS Foundation and other contributors 4 | */ 5 | !function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h= 0; 24 | } 25 | 26 | function defaultFilename () { 27 | return 'Noteplace_' + date2str(new Date()) + '.json'; 28 | } 29 | 30 | // Start file download. 31 | _('#save').addEventListener('click', function () { 32 | _('#modal-input').value = defaultFilename(); 33 | _('#modal-save').style.display = ''; 34 | _('#exampleModalLabel').innerHTML = 'Save to local file:'; 35 | 36 | _('#modal-save').onclick = function () { 37 | download( 38 | _('#modal-input').value, 39 | JSON.stringify(saveToG()) 40 | ); 41 | }; 42 | _('#modal-list').innerHTML = ''; 43 | }, false); 44 | 45 | // save everything to a single object 46 | function saveToG (add_history = true) { 47 | const G = { 48 | T: _View.state.T, 49 | S: _View.state.S, 50 | nodes: ( 51 | add_history 52 | ? _NODES 53 | : _NODES.filter(node => !node.deleted) 54 | ).map(stripNode), 55 | places: stripPlace() 56 | }; 57 | if (add_history) { 58 | G.history = _HISTORY; 59 | G.history_current_id = _HISTORY_CURRENT_ID; 60 | } 61 | return G; 62 | } 63 | 64 | _G = null; 65 | // load everything from single object 66 | function loadFromG (G) { 67 | console.log('Loading..'); 68 | 69 | _G = G; 70 | 71 | _View.goto(G, false, true); 72 | 73 | // delete _PLACES; 74 | if ('places' in G) { 75 | _PLACES = G.places; 76 | } else { 77 | _PLACES = _PLACES_default; 78 | } 79 | fillPlaces(); 80 | 81 | // applyZoom([1*G.T[0],1*G.T[1]], 1*G.S); 82 | $('.node').remove(); 83 | // delete _NODES; 84 | _NODES = []; 85 | gen_DOMId2nodej(); 86 | 87 | G.nodes.map(stripNode).forEach(node => newNode(node, false, true)); 88 | 89 | 90 | redraw(); 91 | 92 | if ( 'history' in G ) { 93 | _HISTORY = G.history; 94 | if ( 'history_current_id' in G ){ 95 | _HISTORY_CURRENT_ID = G.history_current_id; 96 | } else { 97 | _HISTORY_CURRENT_ID = lastHistoryID(); 98 | } 99 | genHistIDMap(); 100 | fillHistoryList(); 101 | } else { 102 | clearAllHistory(); 103 | } 104 | 105 | console.log('Loading complete, now ' + _NODES.length + ' nodes'); 106 | } 107 | 108 | _('#file').oninput = function () { 109 | let fr = new FileReader(); 110 | fr.onload = function () { 111 | console.log('Received file..'); 112 | 113 | loadFromG(JSON.parse(fr.result)); 114 | 115 | $('#file').value = ''; 116 | }; 117 | 118 | fr.readAsText(this.files[0]); 119 | }; 120 | 121 | 122 | -------------------------------------------------------------------------------- /freehand.js: -------------------------------------------------------------------------------- 1 | class FreeHand{ 2 | status = null;// null / 'ready' / 'drawing' 3 | points = []; 4 | path = null; 5 | svg = null; 6 | 7 | static svgns = 'http://www.w3.org/2000/svg'; 8 | 9 | stop(){ 10 | this.status = null; 11 | this.toggleButton(false); 12 | this.fieldSetVisible(false); 13 | } 14 | 15 | ready(){ 16 | this.status = 'ready'; 17 | this.points = []; 18 | this.toggleButton(true); 19 | this.fieldSetVisible(true); 20 | this.createSVG(this); 21 | } 22 | 23 | toggleButton(go){ 24 | var cl = _('#btnFreehand').classList; 25 | if(go){ 26 | cl.add('btn-primary'); 27 | cl.remove('btn-outline-primary'); 28 | }else{ 29 | cl.remove('btn-primary'); 30 | cl.add('btn-outline-primary'); 31 | } 32 | } 33 | 34 | fieldSetVisible(vis){ 35 | _('#freehandField').style.display = vis?'':'none'; 36 | } 37 | 38 | createSVG(T){ 39 | T.svg = document.createElementNS(T.svgns, 'svg'); 40 | T.svg.id = 'freehandsvg'; 41 | T.svg.setAttributeNS(null, 'width', width); 42 | T.svg.setAttributeNS(null, 'height', height); 43 | 44 | T.path = document.createElementNS(T.svgns, 'path'); 45 | T.path.setAttributeNS( null, 'stroke', 'blue'); 46 | T.path.setAttributeNS(null, 'fill', 'none'); 47 | T.path.setAttributeNS(null, 'strokeWidth', '2'); 48 | T.svg.appendChild(T.path); 49 | 50 | // for some reason svg is not properly added otherwise. 51 | // _('#freehandField').appendChild(T.svg); 52 | _('#freehandField').innerHTML += T.svg.outerHTML; 53 | T.svg = _('#freehandsvg'); 54 | T.path = _FreeHand.svg.childNodes[0]; 55 | 56 | } 57 | 58 | draw(T) { 59 | if (T.points.length > 3) { 60 | 61 | T.path.setAttributeNS( 62 | null, 63 | 'd', 64 | 'M ' + T.points.map(a => a[0] + ' ' + a[1]).join(' L') 65 | ); 66 | 67 | } 68 | } 69 | 70 | constructor() { 71 | var T = this 72 | _('#btnFreehand').onclick = function() { 73 | if (T.status) { 74 | T.stop(); 75 | } else { 76 | T.ready(); 77 | } 78 | } 79 | 80 | _('#freehandField').onmouseup = function (e) { 81 | console.log(e); 82 | // return 0 83 | T.stop(); 84 | e.stopPropagation(); 85 | 86 | const minPs = [0,0]; 87 | const maxPs = [0,0]; 88 | const d = 10; 89 | [0,1].forEach(j => { 90 | minPs[j] = -d + Math.min.apply(Math, T.points.map(a => a[j])); 91 | T.points.forEach(a => { a[j] -= minPs[j];}); 92 | maxPs[j] = Math.max.apply(Math, T.points.map(a => a[j])); 93 | }); 94 | T.draw(T); 95 | T.svg.setAttribute('width', maxPs[0] + d ) 96 | T.svg.setAttribute('height', maxPs[1] + d ) 97 | T.svg.id = ""; 98 | 99 | // setTimeout(function(){ 100 | applyAction({ 101 | type: 'A', 102 | 103 | nodes: [{ 104 | text: T.svg.outerHTML, 105 | mousePos: [ minPs[0] , minPs[1]], 106 | style: { color: '#0000ff', strokeWidth: 5, fill:'none'}, 107 | }] 108 | }) 109 | // }, 50); 110 | 111 | T.svg.parentElement.removeChild(T.svg); 112 | } 113 | 114 | _('#freehandField').onmousedown = function (e) { 115 | T.status = 'drawing'; 116 | e.stopPropagation(); 117 | } 118 | 119 | _('#freehandField').onmousemove = function (e) { 120 | if( T.status == 'drawing'){ 121 | T.points.push([e.clientX, e.clientY]); 122 | T.draw(T); 123 | } 124 | e.stopPropagation(); 125 | } 126 | 127 | console.log("Created a FreeHand"); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /localStorage.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _localStorage = { 5 | nodeIDs:{ 6 | save: function() { 7 | localStorage[_localStorage.keys.node_ids] = JSON.stringify( 8 | _NODES.map((node) => node.id) 9 | ) 10 | }, 11 | load: function() { 12 | return JSON.parse(localStorage[_localStorage.keys.node_ids]); 13 | }, 14 | }, 15 | keys:{ 16 | node_ids: 'noteplace.node_ids', 17 | node: { 18 | byId: function getNodeIdLocalStorageKey(node_id){ 19 | return 'noteplace.node_' + node_id 20 | }, 21 | byNode: function getNodeLocalStorageKey(node){ 22 | return _localStorage.keys.node.byId(node.id) 23 | }, 24 | byDom: function getNodeDomLocalStorageKey(dom) { 25 | // but shouldnt it be _localStorage.keys.node.byNode(domNode(dom)) ? 26 | return 'noteplace.' + dom.id; 27 | }, 28 | } 29 | }, 30 | getSize: function localStorageSize (verbose=false) { 31 | let _lsTotal = 0; 32 | let _xLen = 0; 33 | let _x = 0; 34 | for (_x in localStorage) { 35 | if (!localStorage.hasOwnProperty(_x)) { 36 | continue 37 | } 38 | _xLen = ((localStorage[_x].length + _x.length) * 2); 39 | _lsTotal += _xLen; 40 | if (verbose) { 41 | console.log(_x.substr(0, 50) + ' = ' + (_xLen / 1024).toFixed(2) + ' KB'); 42 | } 43 | } 44 | if (verbose) { 45 | console.log('Total = ' + (_lsTotal / 1024).toFixed(2) + ' KB'); 46 | } 47 | return _lsTotal; 48 | }, 49 | removeNode: function removeNodeFromLocalStorage(node){ 50 | localStorage.removeItem(_localStorage.getNodeKey(node)); 51 | }, 52 | node:{ 53 | save: function saveNode(node){ 54 | if (isString(node)) { 55 | node = idNode(node); 56 | } 57 | if (isDom(node)) { 58 | node = domNode(node); 59 | } 60 | if (isNode(node)) { 61 | localStorage[_localStorage.keys.node.byNode(node)] = JSON.stringify(stripNode(node)); 62 | } else { 63 | log(node); 64 | throw Error('What did you aim for, calling save('+node+') ??'); 65 | } 66 | }, 67 | load: function loadNodeById(id){ 68 | return JSON.parse(localStorage[_localStorage.keys.node.byId(id)]); 69 | }, 70 | }, 71 | 72 | save: function save (node = null, save_ids = true) { 73 | // save node state to localStorage. 74 | // if node === null, saves all nodes 75 | // if node == 'ids', saves ids 76 | // additional argument: 77 | // save_ids [bool] : true => save ids too 78 | var nodes2save = [node]; 79 | if (node === null) { 80 | nodes2save = _NODES; 81 | save_ids = true; 82 | } else if ( typeof(node) === 'string'){ 83 | if (node === 'ids') { 84 | save_ids = true; 85 | nodes2save = []; 86 | } 87 | } else if (Array.isArray(node)) { 88 | nodes2save = node; 89 | save_ids = true; 90 | } 91 | 92 | nodes2save.forEach(_localStorage.node.save) 93 | 94 | if (save_ids) { 95 | _localStorage.nodeIDs.save() 96 | } 97 | 98 | _localStorage.places.save(); 99 | 100 | localStorage['noteplace.history'] = JSON.stringify( _HISTORY ); 101 | localStorage['noteplace.history_current'] = _HISTORY_CURRENT_ID; 102 | 103 | const tsize = _localStorage.getSize() / (1024 * 1024); 104 | if (tsize > 4) { 105 | console.error('localStorage ' + tsize.toFixed(3) + ' MB, limit is 5. you know what to do.'); 106 | } 107 | }, 108 | 109 | places_key:'noteplace.places', 110 | places:{ 111 | save: function(){ 112 | localStorage[_localStorage.places_key] = JSON.stringify( 113 | stripPlace(_PLACES) 114 | ); 115 | }, 116 | load: function(){ 117 | return JSON.parse(localStorage[places_key]); 118 | } 119 | }, 120 | 121 | } 122 | 123 | save = _localStorage.save; 124 | 125 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function clientToStatePos(clientPos, state){ 4 | return [ 5 | state.T[0] + clientPos[0] / state.S, 6 | state.T[1] + clientPos[1] / state.S 7 | ] 8 | } 9 | 10 | function stateToClientPos(pos, state){ 11 | return [ 12 | (pos[0] - state.T[0]) * state.S, 13 | (pos[1] - state.T[1]) * state.S 14 | ] 15 | } 16 | 17 | let _View = { 18 | state:{ 19 | T: [0, 0], 20 | S: 1, 21 | }, 22 | getState: function getState(){ 23 | return copy(_View.state); 24 | }, 25 | 26 | preview: { 27 | saved: { 28 | node: null, 29 | oldState: { T: [0, 0], S: 1 }, 30 | }, 31 | }, 32 | 33 | clientToPos: function(x, y) { 34 | var pos = [x,y]; 35 | if (y==undefined) { 36 | pos = x; 37 | } 38 | return clientToStatePos(pos, _View.state); 39 | }, 40 | 41 | posToClient: function(x, y) { 42 | var pos = [x,y]; 43 | if (y==undefined) { 44 | pos = x; 45 | } 46 | return stateToClientPos(pos, _View.state); 47 | 48 | }, 49 | 50 | 51 | changeZoom: function changeViewZooom(Scoef, centerClientPos){ 52 | mousePos = _View.clientToPos(centerClientPos); 53 | _View.state.S = fitInBorders( _View.state.S * Scoef, zoomMin, zoomMax); 54 | _View.applyZoom( 55 | [ 56 | mousePos[0] - centerClientPos[0] / _View.state.S, 57 | mousePos[1] - centerClientPos[1] / _View.state.S 58 | ], 59 | _View.state.S, 60 | true 61 | ); 62 | }, 63 | 64 | getStateURL: function getStateURL (state = null) { 65 | if (state == null) { 66 | state = currentState(); 67 | } 68 | return '?Tx=' + state.T[0] + '&Ty=' + state.T[1] + '&S=' + state.S; 69 | }, 70 | 71 | applyZoom: function applyZoom (T_, S_, smooth = true, noTemp = false) { 72 | console.log('S=' + _View.state.S + ' S_=' + S_); 73 | _View.state.T = [1 * T_[0], 1 * T_[1]]; 74 | 75 | const ds = 0.2 + Math.abs(Math.log10(_View.state.S / S_)); 76 | console.log('ds=' + ds); 77 | _View.state.S = 1 * S_; 78 | 79 | if (smooth) { 80 | setTransitionDur(ds); 81 | } 82 | 83 | status(_View.state); 84 | 85 | redraw(); 86 | 87 | if (noTemp) { 88 | _View.preview.saved.oldState = { T: T_, S: S_ }; 89 | } 90 | 91 | if (smooth) { 92 | clearTimeout(_View.applyZoom_zoomResetTimeout); 93 | _View.applyZoom_zoomResetTimeout = setTimeout(function () { 94 | setTransitionDur(0); 95 | }, 1000 * ds); 96 | } 97 | }, 98 | applyZoom_zoomResetTimeout : null, 99 | applyZoom_lastSmooth : false, 100 | 101 | goto: function gotoState (state, smooth = false, rewrite_preview = false) { 102 | _View.applyZoom(state.T, state.S, smooth, rewrite_preview); 103 | }, 104 | 105 | isBoxSeen: function isBoxSeen(x, xMax, y, yMax) { 106 | if (xMax===undefined) { 107 | yMax = x[3]; 108 | y = x[2]; 109 | xMax = x[1]; 110 | x=x[0]; 111 | } 112 | return isInBox(x, xMax, y, yMax, 113 | _View.state.T[0] - width * 0.5 / _View.state.S, _View.state.T[0] + width * 1.5 / (1 * _View.state.S), 114 | _View.state.T[1] - height * 0.5 / _View.state.S, _View.state.T[1] + width * 1.5 / (1 * _View.state.S) 115 | ) 116 | }, 117 | 118 | gotoURL: function zoomToURL (s, smooth = true, noTemp = false) { 119 | const urlParams = new URLSearchParams(s); 120 | _View.applyZoom( 121 | [1 * urlParams.get('Tx'), 1 * urlParams.get('Ty')] 122 | , 1 * urlParams.get('S') ? 1 * urlParams.get('S') : 1 123 | , smooth 124 | , noTemp 125 | ); 126 | }, 127 | 128 | 129 | gotoNode: function gotoNode (node) { 130 | gotoState(nodeState(node), false, true); 131 | depreviewNode(); 132 | }, 133 | 134 | previewState: function previewState(state){ 135 | // accepts state as 136 | // - {T: .., S:..} 137 | // - JSON string 138 | // - URLSearchParams string 139 | if (typeof (state) === 'string') { 140 | state = parseStateFromString(state); 141 | } 142 | 143 | _View.preview.saved.oldState = currentState(); 144 | 145 | _View.goto(state, false, false); 146 | }, 147 | 148 | 149 | previewNode: function previewNode (node) { 150 | _View.previewState(nodeState(node)); 151 | 152 | node.dom.classList.add('np-search-preview'); 153 | }, 154 | 155 | exitPreview: function exitPreview(){ 156 | _View.goto(_View.preview.saved.oldState, false, true) 157 | } 158 | 159 | } 160 | 161 | zoomToURL = _View.gotoURL; 162 | gotoState = _View.goto; 163 | gotoNode = _View.gotoNode; 164 | getStateURL = _View.getStateURL; 165 | exitPreview = _View.exitPreview; 166 | previewState = _View.previewState; 167 | posToClient = _View.posToClient; 168 | clientToPos = _View.clientToPos; 169 | previewNode = _View.previewNode; 170 | 171 | function currentState(){ 172 | return copy(_View.state); 173 | } 174 | 175 | function parseStateFromString(stateString){ 176 | try { 177 | state = JSON.parse(stateString); 178 | state = { T: [state.T[0] * 1, state.T[1] * 1], S: state.S * 1 }; 179 | } catch (e) { 180 | state = new URLSearchParams(stateString); 181 | state = { T: [state.get('Tx') * 1, state.get('Ty') * 1], S: state.get('S') * 1 }; 182 | } 183 | return state 184 | } 185 | 186 | function nodeState (node) { 187 | let hS = 20 / node.fontSize; 188 | return { 189 | T: [ 190 | (node.xMax ? (node.x + node.xMax) / 2 : (node.x + node.text.length * node.fontSize * 0.4)) - width / (3 * hS), 191 | node.y - height / (3 * hS) 192 | ], 193 | S: hS 194 | }; 195 | } 196 | 197 | 198 | function depreviewNode () { 199 | exitPreview(); 200 | 201 | $('.np-search-preview').removeClass('np-search-preview'); 202 | } 203 | 204 | -------------------------------------------------------------------------------- /jquery.ui.rotatable.min.js: -------------------------------------------------------------------------------- 1 | (function(c, d) { 2 | c.widget("ui.rotatable", c.ui.mouse, { 3 | options: { 4 | handle: !1, 5 | angle: !1, 6 | snap: !1, 7 | step: 22.5, 8 | rotationCenterX: !1, 9 | rotationCenterY: !1, 10 | start: null, 11 | rotate: null, 12 | stop: null 13 | }, 14 | rotationCenterX: function(a) { 15 | if (a === d) 16 | return this.options.rotationCenterX; 17 | this.options.rotationCenterX = a 18 | }, 19 | rotationCenterY: function(a) { 20 | if (a === d) 21 | return this.options.rotationCenterY; 22 | this.options.rotationCenterY = a 23 | }, 24 | handle: function(a) { 25 | if (a === d) 26 | return this.options.handle; 27 | this.options.handle = a 28 | }, 29 | angle: function(a) { 30 | if (a === d) 31 | return this.options.angle; 32 | this.elementCurrentAngle = this.options.angle = a; 33 | this.performRotation(this.options.angle) 34 | }, 35 | _create: function() { 36 | var a; 37 | this.options.handle ? a = this.options.handle : (a = c(document.createElement("div")), 38 | a.addClass("ui-rotatable-handle")); 39 | this.listeners = { 40 | rotateElement: c.proxy(this.rotateElement, this), 41 | startRotate: c.proxy(this.startRotate, this), 42 | stopRotate: c.proxy(this.stopRotate, this), 43 | wheelRotate: c.proxy(this.wheelRotate, this) 44 | }; 45 | //this.element.bind("wheel",this.listeners.wheelRotate); 46 | a.draggable({ 47 | helper: "clone", 48 | start: this.dragStart, 49 | handle: a 50 | }); 51 | a.bind("mousedown", this.listeners.startRotate); 52 | a.appendTo(this.element); 53 | 0 != this.options.angle ? (this.elementCurrentAngle = this.options.angle, 54 | this.performRotation(this.elementCurrentAngle)) : this.elementCurrentAngle = 0 55 | }, 56 | _destroy: function() { 57 | this.element.removeClass("ui-rotatable"); 58 | this.element.find(".ui-rotatable-handle").remove(); 59 | //this.element.unbind("wheel",this.listeners.wheelRotate) 60 | }, 61 | performRotation: function(a) { 62 | this.element.css("transform-origin", this.options.rotationCenterX + "% " + this.options.rotationCenterY + "%"); 63 | this.element.css("-ms-transform-origin", this.options.rotationCenterX + "% " + this.options.rotationCenterY + "%"); 64 | this.element.css("-webkit-transform-origin", this.options.rotationCenterX + "% " + this.options.rotationCenterY + "%"); 65 | this.element.css("transform", "rotate(" + a + "rad)"); 66 | this.element.css("-moz-transform", "rotate(" + a + "rad)"); 67 | this.element.css("-webkit-transform", "rotate(" + a + "rad)"); 68 | this.element.css("-o-transform", "rotate(" + a + "rad)") 69 | }, 70 | getElementOffset: function() { 71 | this.performRotation(0); 72 | var a = this.element.offset(); 73 | this.performRotation(this.elementCurrentAngle); 74 | return a 75 | }, 76 | getElementCenter: function() { 77 | var a = this.getElementOffset(); 78 | if (!1 === this.options.rotationCenterX) 79 | var b = a.left + this.element.width() / 2 80 | , a = a.top + this.element.height() / 2; 81 | else 82 | b = a.left + this.element.width() / 100 * this.options.rotationCenterX, 83 | a = a.top + this.element.height() / 100 * this.options.rotationCenterY; 84 | return [b, a] 85 | }, 86 | dragStart: function(a) { 87 | if (this.element) 88 | return !1 89 | }, 90 | startRotate: function(a) { 91 | var b = this.getElementCenter(); 92 | this.mouseStartAngle = Math.atan2(a.pageY - b[1], a.pageX - b[0]); 93 | this.elementStartAngle = this.elementCurrentAngle; 94 | this.hasRotated = !1; 95 | this._propagate("start", a); 96 | c(document).bind("mousemove", this.listeners.rotateElement); 97 | c(document).bind("mouseup", this.listeners.stopRotate); 98 | return !1 99 | }, 100 | rotateElement: function(a) { 101 | if (!this.element || this.element.disabled) 102 | return !1; 103 | var b = this.getRotateAngle(a); 104 | this.performRotation(b); 105 | var c = this.elementCurrentAngle; 106 | this.elementCurrentAngle = b; 107 | this._propagate("rotate", a); 108 | c != b && (this._trigger("rotate", a, this.ui()), 109 | this.hasRotated = !0); 110 | return !1 111 | }, 112 | stopRotate: function(a) { 113 | if (this.element && !this.element.disabled) 114 | return c(document).unbind("mousemove", this.listeners.rotateElement), 115 | c(document).unbind("mouseup", this.listeners.stopRotate), 116 | this.elementStopAngle = this.elementCurrentAngle, 117 | this.hasRotated && this._propagate("stop", a), 118 | setTimeout(function() { 119 | this.element = !1 120 | }, 10), 121 | !1 122 | }, 123 | getRotateAngle: function(a) { 124 | var b = this.getElementCenter(); 125 | a = Math.atan2(a.pageY - b[1], a.pageX - b[0]) - this.mouseStartAngle + this.elementStartAngle; 126 | this.options.snap && (a = a / Math.PI * 180, 127 | a = Math.round(a / this.options.step) * this.options.step, 128 | a = a * Math.PI / 180); 129 | return a 130 | }, 131 | wheelRotate: function(a) { 132 | var b = Math.round(a.originalEvent.deltaY / 10) * Math.PI / 180 133 | , b = this.elementCurrentAngle + b; 134 | this.angle(b); 135 | this._trigger("rotate", a, this.ui()) 136 | }, 137 | _propagate: function(a, b) { 138 | c.ui.plugin.call(this, a, [b, this.ui()]); 139 | "rotate" !== a && this._trigger(a, b, this.ui()) 140 | }, 141 | plugins: {}, 142 | ui: function() { 143 | return { 144 | api: this, 145 | element: this.element, 146 | angle: { 147 | start: this.elementStartAngle, 148 | current: this.elementCurrentAngle, 149 | stop: this.elementStopAngle 150 | } 151 | } 152 | } 153 | }) 154 | } 155 | )(jQuery); 156 | -------------------------------------------------------------------------------- /nodes_default.js: -------------------------------------------------------------------------------- 1 | nodes_default = [ 2 | {id: "0", x: "0", y: "0", fontSize: "12", text: "test0"} 3 | ,{id: "1", x: "100", y: "100", fontSize: "12", text: "test1"} 4 | ,{id: "2", x: "222.9654", y: "101.16120000000001", fontSize: "19.53125", text: "# Welcome to Noteplace\nHere you _will_ find a place for **each** of your notes"} 5 | ,{id: "3", x: "177.71582669914915", y: "84.10224103813428", fontSize: "4.204482076268574", text: "test3"} 6 | ,{id: "4", x: "173.18169898809143", y: "83.77972083086841", fontSize: "1.7677669529663695", text: "test4"} 7 | ,{id: "5", x: "172.25362133778404", y: "83.82391500469258", fontSize: "0.8838834764831848", text: "test5"} 8 | ,{id: "6", x: "171.81167959954246", y: "83.89020626542883", fontSize: "0.4419417382415924", text: "test6"} 9 | ,{id: "7", x: "171.69807758002312", y: "83.84816472660286", fontSize: "0.039062499999999965", text: "test7"} 10 | ,{id: "8", x: "171.58772601752312", y: "83.84914128910286", fontSize: "0.019531249999999983", text: "test8"} 11 | ,{id: "9", x: "171.53938617377312", y: "83.85451238285286", fontSize: "0.009765624999999991", text: "test9"} 12 | ,{id: "10", x: "171.52229633002312", y: "83.85841863285286", fontSize: "0.004882812499999996", text: "test10"} 13 | ,{id: "11", x: "171.5140747601513", y: "83.86304412981941", fontSize: "0.0029033376831121096", text: "test11"} 14 | ,{id: "12", x: "171.5083740342907", y: "83.86542565010431", fontSize: "0.001726334915006218", text: "test12"} 15 | ,{id: "13", x: "171.50607848896962", y: "83.86699217543257", fontSize: "0.000513242440950753", text: "test13"} 16 | ,{id: "14", x: "171.50464071771412", y: "83.86762401331647", fontSize: "0.000513242440950753", text: "test14"} 17 | ,{id: "15", x: "-239.71568160406434", y: "-963.4582672147874", fontSize: "159.99999999999872", text: "test15"} 18 | ,{id: "16", x: "-1407.4692147593846", y: "-1223.4339365451995", fontSize: "226.27416997969334", text: "test16"} 19 | ,{id: "17", x: "-3394.5134876903767", y: "170.21304013896616", fontSize: "905.0966799187728", text: "test17"} 20 | ,{id: "18", x: "-9342.456857155341", y: "-809.2705284670758", fontSize: "2152.6948230494886", text: "test18"} 21 | ,{id: "19", x: "-17675.906689190488", y: "-803.8054286378799", fontSize: "3044.3702144069366", text: "test19"} 22 | ,{id: "20", x: "-34529.21208752887", y: "-3879.757393564678", fontSize: "6088.740428813872", text: "test20"} 23 | ,{id: "113", x: "10412901562002.574", y: "1891828629789.287", fontSize: "2311438465816.513", text: "WOW you're far from home."} 24 | ,{id: "115", x: "214123162955070.66", y: "10711632881380.672", fontSize: "31098885119754.062", text: "I mean. WOW."} 25 | ,{id: "116", x: "1104592423952645", y: "-81819586322754.16", fontSize: "209207528124706.03", text: "Is anyone seeing this??"} 26 | ,{id: "117", x: "27359550959356268", y: "-1764328513537750", fontSize: "3980657295328512.5", text: "Here's a heart for ya:\n❤️"} 27 | ,{id: "118", x: "339542519644310660", y: "20194450624807310", fontSize: "31845258362628070", text: "Here's a beacon of hope:\n⛯"} 28 | ,{id: "119", x: "1044733020186787100", y: "-43038781628711940", fontSize: "151482431295748700", text: "Now that is just.. stupid"} 29 | ,{id: "120", x: "5369006128692392000", y: "-759527390857764900", fontSize: "720575940379260300", text: "STOP."} 30 | ,{id: "21", x: "-145702.54698230542", y: "-29719.7041179353", fontSize: "36893.488147419106", text: "test21"} 31 | ,{id: "22", x: "-606177.5512478726", y: "-83672.74118472094", fontSize: "151115.72745182866", text: "test22"} 32 | ,{id: "23", x: "-3379973.219546266", y: "-541879.8053350588", fontSize: "990352.0314283043", text: "test23"} 33 | ,{id: "24", x: "-14405760.455843866", y: "-2030180.8381655277", fontSize: "4056481.9207303347", text: "test24"} 34 | ,{id: "25", x: "-136028953.80337054", y: "-12309370.929006876", fontSize: "42535295.86511732", text: "test25"} 35 | ,{id: "26", x: "-2239343664.138194", y: "-452770458.33262914", fontSize: "713623846.3529803", text: "test26"} 36 | ,{id: "28", x: "269", y: "266", fontSize: "20", text: "Just try double-clicking on empty space (or text?😊)"} 37 | ,{x: 360, y: 314, fontSize: 20, text: 'Text is MarkDown:\n\n`#` For headlines\n`_`for _cursive_`_`\n`**`for **bold**`**`\n`[`Link`](`http://url` ` ` `"`title`"``)` for [links](https://jonschlinkert.github.io/remarkable/demo/ "See Markdown spec here")'} 38 | ,{id: "30", x: "26", y: "408", fontSize: "20", text: "Now try\nmouse-wheel-zooming\nin and out!"} 39 | ,{id: "31", x: "119.51171875", y: "505.8828125", fontSize: "4.882812499999999", text: "Or click [here](?Tx=0&Ty=-100&S=1) to zoom back"} 40 | ,{id: "32", x: "148", y: "-46", fontSize: "20", text: "Or click [here](?Tx=171.50172055191769&Ty=83.86545213452068&S=126765.06002282312) to navigate in deep"} 41 | ,{id: "33", x: "171.50354230839773", y: "83.86848513630132", fontSize: "0.00015777218104420212", text: "Or click [here](?Tx=0&Ty=0&S=1) to go back"} 42 | ,{id: "27", x: "10.02900390624992", y: "248.26645507812503", fontSize: "16.875", text: "__Left__ mouse button down and drag\nto move around\n__Middle__ mouse button\n to drag text around"} 43 | ,{id: "34", x: "256", y: "544", fontSize: "20", text: "Click to select a note"} 44 | ,{id: "35", x: "146", y: "615", rotate: "0.4018327787670817", fontSize: "20", text: "drag rotate icon (left bottom corner) to rotate, the note"} 45 | ,{x: "80", y: "800", rotate: "0", fontSize: "20", text: "You can Save to/Load from local file,\nso you may just save the whole page and work without internet connection!"} 46 | ,{x: "92", y: "900", rotate: "0", fontSize: "20", text: "Or you could use Google Drive if the page is run from eugenpt.gihub.io"} 47 | ,{x: "585.974681854248", y: "191.39131927490234", rotate: "0", fontSize: "1.192092895507812", text: "(This place is practically inifinitely zoomable)"} 48 | ,{x: "605.9516071976361", y: "193.21158852783742", rotate: "0", fontSize: "0.11368683772161589", text: "I mean, seriously!"} 49 | ,{x: "606.7511873267723", y: "193.29315530012735", rotate: "1.6987763617720013", fontSize: "0.00173668223966733", text: "Just look a it!"} 50 | ,{x: "1161.7756638639928", y: "159.90205224000033", rotate: "-0.6054638070325069", fontSize: "60.67819999999919", text: "![Minion](https://octodex.github.com/images/minion.png)"} 51 | ,{x: "1126.3357461092987", y: "99.72472050558802", rotate: "0", fontSize: "31.999999999999762", text: "You can even embed images "} 52 | ,{x: "982.9757461092998", y: "476.68472050558523", rotate: "0", fontSize: "31.999999999999762", text: "(double-click on it to see the code that shows the image)"} 53 | ,{x: 1033.4454329600035, y: 742.3029999999987, fontSize: "26.21440000000001", text: "And copy-paste"} 54 | ,{x: 894.8548763193781, y: 606.4862402343737, fontSize: "31.249999999999996", text: "You can also select multiple notes\nwith a rectangle ~using~ ~shift-drag~"} 55 | ,{text: "(including images!)", x: 1138.3080763193782, y: 821.1506402343733, fontSize: "26.214399999999998"} 56 | ,{text: "(including from ++outside++ ***Noteplace***!)", x: 954.8230763193785, y: 785.3856402343732, fontSize: "24.999999999999996"} 57 | ,{x: 528.92, y: 505.31999999999994, fontSize: "12.8", text: "(for full **MarkDown** spec follow that link)"} 58 | ]; 59 | 60 | default_node_style = { 61 | textAlign:'left' 62 | ,border:'none' 63 | ,color:'#000000' 64 | ,backgroundColor:'transparent' 65 | } -------------------------------------------------------------------------------- /search.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | */ 8 | 9 | let _SEARCH = { 10 | page_results: 20, 11 | q: '', 12 | results: [], 13 | last_checked_nodej: -1, 14 | last_dom_nodej: -1, 15 | current_page: 0, 16 | dom: [] 17 | }; 18 | 19 | let _NODES_searchRes = []; 20 | 21 | function findPlaces(q, places=null){ 22 | const R = []; 23 | if( places === null ) { 24 | places = _PLACES.items; 25 | } 26 | for( let place of places ) { 27 | if (searchMatch(q, place.name)){ 28 | R.push(place); 29 | } 30 | if ('items' in place) { 31 | findPlaces(q, place.items).forEach( x => R.push(x) ) 32 | } 33 | } 34 | return R; 35 | } 36 | 37 | function fillPlaceSearchResults () { 38 | const root = _('#searchResultContainer'); 39 | const placeSearchResults = findPlaces(_SEARCH.q); 40 | 41 | for( let place of placeSearchResults) { 42 | const div = _ce('div' 43 | , 'className', 'container btn btn-outline-secondary my-1' 44 | ); 45 | 46 | 47 | if ('items' in place) { 48 | div.dataset['path'] = place.dom.hBtn.dataset['path']; 49 | div.title = "Places folder, click to see it in places tree" 50 | div.onmouseenter = function (e) { 51 | previewPlacePath(this.dataset.path); 52 | } 53 | div.onmouseleave = function (e) { 54 | depreviewPlace(); 55 | } 56 | div.onclick = function (e) { 57 | showPlacePath(this.dataset.path); 58 | } 59 | } else { 60 | div.dataset['path'] = place.dom.a.dataset['path']; 61 | div.onmouseenter = function (e) { 62 | previewState(pathPlace(this.dataset['path']).state); 63 | previewPlacePath(this.dataset['path']); 64 | } 65 | div.onmouseleave = function (e) { 66 | exitPreview(); 67 | depreviewPlace(); 68 | } 69 | div.onclick = function (e) { 70 | gotoState(pathPlace(this.dataset.path).state, false, true); 71 | } 72 | } 73 | 74 | const row = _ce('div' 75 | , 'className', 'row np-sr-row align-items-center justify-content-between' 76 | ); 77 | 78 | const col1 = _ce('div' 79 | , 'className', 'col' 80 | , 'innerHTML', '' 81 | + ('items' in place ? ' ':'  ') 82 | ) 83 | 84 | const col2 = _ce('div' 85 | , 'className', 'col' 86 | , 'innerHTML', place.name 87 | ); 88 | 89 | 90 | row.appendChild(col1); 91 | row.appendChild(col2); 92 | // if (!('items' in place)) { 93 | const col3 = _ce('div' 94 | , 'className', 'col p-0' 95 | , 'innerHTML', '' 96 | ); 97 | const ti = _ce('i' 98 | ,'className', "bi-folder-symlink" 99 | ,'title', 'Show in places tree' 100 | ,'onclick', function (e) { 101 | showPlacePath(div.dataset.path); 102 | e.stopPropagation(); 103 | } 104 | , 'onmouseenter', function (e) { 105 | this.classList.add('text-secondary'); 106 | this.classList.add('bg-light'); 107 | } 108 | , 'onmouseleave', function (e) { 109 | this.classList.remove('text-secondary'); 110 | this.classList.remove('bg-light'); 111 | } 112 | ) 113 | col3.appendChild(ti); 114 | row.appendChild(col3); 115 | // } 116 | 117 | div.appendChild(row); 118 | 119 | root.appendChild(div); 120 | } 121 | 122 | } 123 | 124 | function fillSearchResults () { 125 | const root = _('#searchResultContainer'); 126 | 127 | if (_SEARCH.last_dom_nodej < 0) { 128 | root.innerHTML = ''; 129 | fillPlaceSearchResults(); 130 | } 131 | // for(var j=_SEARCH.last_dom_nodej+1; j<_SEARCH.results.length; j++){ 132 | _SEARCH.results.slice(_SEARCH.last_dom_nodej + 1).forEach((n) => { 133 | // var n=_SEARCH.results[j]; 134 | // _SEARCH.results.forEach((n)=>{ 135 | const div = _ce('div' 136 | , 'className', 'container btn btn-outline-secondary my-1' 137 | , 'onmouseenter', function (e) { 138 | console.log('enter!' + n.text); 139 | previewNode(n); 140 | console.log(currentState()); 141 | } 142 | , 'onmouseleave', function (e) { 143 | console.log('leave! ' + n.text); 144 | depreviewNode(); 145 | console.log(currentState()); 146 | } 147 | , 'onclick', function (e) { 148 | console.log('click! ' + n.text); 149 | depreviewNode(); 150 | gotoNode(n); 151 | console.log(currentState()); 152 | } 153 | ); 154 | const row = _ce('div' 155 | , 'className', 'row np-sr-row' 156 | ); 157 | 158 | const col = _ce('div' 159 | , 'className', 'col' 160 | , 'innerHTML', n.text 161 | ); 162 | 163 | row.appendChild(col); 164 | div.appendChild(row); 165 | 166 | root.appendChild(div); 167 | 168 | _SEARCH.last_dom_nodej++; 169 | }); 170 | } 171 | 172 | function searchMatch (q, s) { 173 | return q.split(' ').every(jq => RegExp(jq, 'gi').test(s)); 174 | // simpler: 175 | // q.split(' ').every(jq=>s.toLowerCase().indexOf(jq)>=0) 176 | } 177 | 178 | function findSearchResults (n = null) { 179 | console.log('findSearchResults'); 180 | if (n === null) { 181 | n = _SEARCH.page_results; 182 | } 183 | 184 | n = n + _SEARCH.results.length; 185 | while ((_SEARCH.results.length < n) 186 | &&(_SEARCH.last_checked_nodej < _NODES.length - 1)) { 187 | _SEARCH.last_checked_nodej++; 188 | if (searchMatch(_SEARCH.q, _NODES[_SEARCH.last_checked_nodej].text)) { 189 | _SEARCH.results.push(_NODES[_SEARCH.last_checked_nodej]); 190 | } 191 | } 192 | console.log('/findSearchResults _SEARCH.results.length=' + _SEARCH.results.length); 193 | } 194 | 195 | function onSearchInput (q) { 196 | exitPreview(); 197 | depreviewNode(); 198 | depreviewPlace(); 199 | 200 | console.log('onSearchInput q=' + q); 201 | _SEARCH.q = q; 202 | _SEARCH.last_dom_nodej = -1; // to restart 203 | if (q) { 204 | _SEARCH.results = []; 205 | _SEARCH.last_checked_nodej = -1; 206 | findSearchResults(); 207 | // _SEARCH.results = _NODES.filter((n)=>__isin_all(q,n.text)) 208 | } else { 209 | _SEARCH.results = _NODES; 210 | _SEARCH.last_checked_nodej = -1; 211 | } 212 | 213 | fillSearchResults(); 214 | } 215 | 216 | _('#searchInput').addEventListener('input', function (e) { 217 | onSearchInput(e.target.value); 218 | }); 219 | 220 | _('#searchInput').addEventListener('keydown', function (e) { 221 | console.log('searchInput keydown'); 222 | console.log(e); 223 | if (e.code == 'Escape') { 224 | e.preventDefault(); 225 | e.stopPropagation(); 226 | _('#search-toggle').click(); 227 | } 228 | }); 229 | 230 | _('#search-toggle').onclick = function (e) { 231 | e.preventDefault(); 232 | e.stopPropagation(); 233 | if (_('#searchSideBar').classList.contains('toggled')) { 234 | _('#searchSideBar').classList.remove('toggled'); 235 | _('#searchInput').focus(); 236 | } else { 237 | _('#searchSideBar').classList.add('toggled'); 238 | } 239 | e.stopPropagation(); 240 | }; 241 | 242 | _('#searchSideBar').onmousedown = function (e) { 243 | e.stopPropagation(); 244 | }; 245 | _('#searchSideBar').onmouseup = function (e) { 246 | e.stopPropagation(); 247 | }; 248 | _('#searchSideBar').onmousewheel = function (e) { 249 | e.stopPropagation(); 250 | }; 251 | 252 | _('#searchResultContainer').addEventListener('scroll', function (e) { 253 | console.log(e); 254 | console.log(this.scrollTop); 255 | console.log(this.scrollHeight); 256 | 257 | if (this.scrollTop + this.clientHeight + this.scrollHeight - 100) { 258 | findSearchResults(); 259 | fillSearchResults(); 260 | } 261 | }); 262 | -------------------------------------------------------------------------------- /universal.js: -------------------------------------------------------------------------------- 1 | const ALPHANUMERIC = '0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'; 2 | 3 | function rand0Z () { 4 | return ALPHANUMERIC[Math.floor(Math.random() * ALPHANUMERIC.length)]; 5 | } 6 | 7 | // 8 | function newID(start, checkfun, addfun=null, maxTries=1000) { 9 | let id = start || ''; 10 | addfun = addfun || rand0Z; 11 | 12 | let nTries = 0; 13 | while (checkfun(id)) { 14 | id = id + addfun(); 15 | nTries++; 16 | if (nTries > maxTries) { 17 | throw Error('newID: Maximum number of tries exceeded, check your checkfun!'); 18 | } 19 | } 20 | return id; 21 | } 22 | 23 | const log = console.log; 24 | 25 | 26 | function copy(x){ 27 | if(Array.isArray(x)){ 28 | return x.map(copy); 29 | }else if(x instanceof Object){ 30 | var r = {}; 31 | for(var j in x){ 32 | r[j] = copy(x[j]); 33 | } 34 | return r; 35 | }else{ 36 | return x; 37 | } 38 | } 39 | 40 | function now () { 41 | return new Date().getTime(); 42 | } 43 | 44 | function int2strwz(i, length) { 45 | return (Array(10).join('0')+i).slice(-length); 46 | } 47 | 48 | function date2str(d) { 49 | return d.getFullYear() + '' 50 | + int2strwz(d.getMonth()+1,2) + '' 51 | + int2strwz(d.getDate(), 2) + '-' 52 | + int2strwz(d.getHours(), 2) + '' 53 | + int2strwz(d.getMinutes(), 2) + '' 54 | + int2strwz(d.getSeconds(), 2) 55 | } 56 | 57 | function arrayMapJ(a){ 58 | const R = new Map(); 59 | for (let j = 0 ; j < a.length ; j ++) { 60 | R.set(a[j], j); 61 | } 62 | return R; 63 | } 64 | 65 | function equalSetsOfItems(a1, a2){ 66 | const s1 = [...arrayMapJ(a1).keys()].sort(); 67 | const s2 = [...arrayMapJ(a2).keys()].sort(); 68 | if( s1.length != s2.length ) { 69 | return false; 70 | } 71 | for( let j = 0 ; j < s1.length ; j++) { 72 | if (s1[j] != s2[j]) { 73 | return false; 74 | } 75 | } 76 | return true; 77 | } 78 | 79 | Array.prototype.contains = function(elt) { 80 | return this.indexOf(elt)>=0; 81 | } 82 | 83 | function Max() { 84 | var a = arguments.length==1 ? arguments[0] : arguments; 85 | return Math.max.apply(Math,a); 86 | } 87 | 88 | function Min() { 89 | var a = arguments.length==1 ? arguments[0] : arguments; 90 | return Math.min.apply(Math,a); 91 | } 92 | 93 | function listForEach(arr, fun){ 94 | [].forEach.call(arr, fun); 95 | } 96 | 97 | function listMap(arr, fun) { 98 | return [].map.call(arr, fun); 99 | } 100 | 101 | // add map and forEach to some DOM-related array-like thingies 102 | ['map','forEach','slice'].forEach( (fun_name) => { 103 | [NodeList, HTMLCollection].forEach( (obj) => { 104 | obj.prototype[fun_name] = function() { return [...this][fun_name](...arguments); }; 105 | }) 106 | }); 107 | 108 | String.prototype.pxToFloat = function(){ 109 | return this.slice(0,-2) * 1.0; 110 | } 111 | String.prototype.toPx = function(){ 112 | return this+'px'; 113 | } 114 | Number.prototype.toPx = function(){ 115 | return this+'px'; 116 | } 117 | 118 | // if property is dot-separated (style.color for example) 119 | // take obj.style.color instead of just obj['style.color'] 120 | function dotProp(obj, prop){ 121 | const propParts = prop.split('.'); 122 | let h = obj; 123 | for(let propPart of propParts){ 124 | if(!(propPart in h)){ 125 | h[propPart] = {}; 126 | } 127 | h = h[propPart]; 128 | } 129 | return h; 130 | } 131 | 132 | function setDotProp(obj, prop, value){ 133 | const propParts = prop.split('.'); 134 | dotProp(obj, propParts.slice(0,-1).join('.'))[propParts[propParts.length-1]] = value; 135 | } 136 | 137 | // // This is fun but it's getting called by jQuery for some reason. 138 | // Object.prototype.ep_get = function (prop) { 139 | // return dotProp(this, prop); 140 | // } 141 | // Object.prototype.ep_set = function (prop, value) { 142 | // return setDotProp(this, prop); 143 | // } 144 | 145 | function delete_defaults (obj, def) { 146 | const r = {}; 147 | for (let p of Object.keys(obj)) { 148 | if ((def[p] === undefined) || (obj[p] !== def[p])) { 149 | r[p] = obj[p]; 150 | } 151 | } 152 | return Object.keys(r).length > 0 ? r : undefined; 153 | } 154 | 155 | function isDotInBox(dotPos, boxPos){ 156 | return ( 157 | (dotPos[0] >= boxPos[0][0]) 158 | &&(dotPos[0] <= boxPos[0][1]) 159 | &&(dotPos[1] >= boxPos[1][0]) 160 | &&(dotPos[1] <= boxPos[1][1]) 161 | ); 162 | } 163 | 164 | function min(a,b){ 165 | return (a>b)?b:a; 166 | } 167 | function max(a,b){ 168 | return (a= min(bxMin,bxMax)) 184 | && (max(yMin,yMax) >= min(byMin,byMax)) 185 | ); 186 | } 187 | 188 | __isin = (q, s) => s.indexOf(q) >= 0 189 | __isin_all = (q, s) => q.split(' ').every(jq => __isin(jq, s)) 190 | 191 | function toStr (a) { 192 | return (typeof (a) === 'number') 193 | ? a.toExponential(2) 194 | : (Array.isArray(a) 195 | ? ('[' + a.map(toStr).join(', ') + ']') 196 | : ((typeof (a) === 'object') 197 | ? '{' + Object.keys(a).map(k => k + ':' + toStr(a[k])).join(', ') + '}' 198 | : a) 199 | ); 200 | } 201 | 202 | function isString(obj){ 203 | return typeof(obj) === 'string'; 204 | } 205 | 206 | // ::::::::: :::::::: :::: :::: 207 | // :+: :+: :+: :+: +:+:+: :+:+:+ 208 | // +:+ +:+ +:+ +:+ +:+ +:+:+ +:+ 209 | // +#+ +:+ +#+ +:+ +#+ +:+ +#+ 210 | // +#+ +#+ +#+ +#+ +#+ +#+ 211 | // #+# #+# #+# #+# #+# #+# 212 | // ######### ######## ### ### 213 | 214 | function isDom(obj){ 215 | return 'click' in obj 216 | } 217 | 218 | function _ (s) { 219 | if (s[0] === '#') { 220 | return document.getElementById(s.slice(1)); 221 | } else if (s[0] === '.') { 222 | return document.getElementsByClassName(s.slice(1)); 223 | } else { 224 | throw Error('Not Implemented: selector=[' + s + ']'); 225 | } 226 | } 227 | 228 | // create element 229 | function _ce (tag, plopName='className', plopVal='class') { 230 | const elt = document.createElement(tag); 231 | for (let j = 1; j < arguments.length; j += 2) { 232 | if (arguments[j] in elt){ 233 | elt[arguments[j]] = arguments[j + 1]; 234 | }else{ 235 | setDotProp(elt, arguments[j], arguments[j + 1]); 236 | } 237 | } 238 | return elt; 239 | } 240 | 241 | 242 | const copyToClipboard = str => { 243 | const el = document.createElement('textarea'); 244 | el.value = str; 245 | el.setAttribute('readonly', ''); 246 | el.style.position = 'absolute'; 247 | el.style.left = '-9999px'; 248 | el.style.opacity = 0; 249 | document.body.appendChild(el); 250 | el.select(); 251 | document.execCommand('copy'); 252 | document.body.removeChild(el); 253 | }; 254 | 255 | // https://gist.github.com/simondahla/0c324ba8e6ed36055787 256 | function addOnContentChange (elt, fun) { 257 | // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 258 | // Options for the observer (which mutations to observe) 259 | const config = { 260 | // attributes: true, 261 | childList: true, 262 | subtree: true 263 | }; 264 | 265 | // Create an observer instance linked to the callback function 266 | const observer = new MutationObserver(fun); 267 | 268 | // Start observing the target node for configured mutations 269 | observer.observe(elt, config); 270 | return observer; 271 | } 272 | 273 | function download (filename, text) { 274 | var element = document.createElement('a'); 275 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); 276 | element.setAttribute('download', filename); 277 | 278 | element.style.display = 'none'; 279 | document.body.appendChild(element); 280 | 281 | element.click(); 282 | 283 | document.body.removeChild(element); 284 | } 285 | -------------------------------------------------------------------------------- /gdrive.js: -------------------------------------------------------------------------------- 1 | const CLIENT_ID = '389896466198-ddtgdva4g6in7plssve78g44n9fhsp79.apps.googleusercontent.com'; 2 | const API_KEY = 'AIzaSyCZvopRVLVjU2CZokOGT_oyx-KK_ZF6cJg'; 3 | 4 | // Array of API discovery doc URLs for APIs used by the quickstart 5 | const DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']; 6 | 7 | // Authorization scopes required by the API; multiple scopes can be 8 | // included, separated by spaces. 9 | // var SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly'; 10 | // var SCOPES = 'https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file'; 11 | const SCOPES = 'https://www.googleapis.com/auth/drive.appdata'; 12 | 13 | const authorizeButton = document.getElementById('authorize_button'); 14 | const signoutButton = document.getElementById('signout_button'); 15 | 16 | /** 17 | * On load, called to load the auth2 library and API client library. 18 | */ 19 | function handleClientLoad () { 20 | gapi.load('client:auth2', initClient); 21 | } 22 | 23 | /** 24 | * Initializes the API client library and sets up sign-in state 25 | * listeners. 26 | */ 27 | function initClient () { 28 | gapi.client.init({ 29 | apiKey: API_KEY, 30 | clientId: CLIENT_ID, 31 | discoveryDocs: DISCOVERY_DOCS, 32 | scope: SCOPES 33 | }).then(function () { 34 | // Listen for sign-in state changes. 35 | gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus); 36 | 37 | // Handle the initial sign-in state. 38 | updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get()); 39 | authorizeButton.onclick = handleAuthClick; 40 | signoutButton.onclick = handleSignoutClick; 41 | }, function (error) { 42 | appendPre(JSON.stringify(error, null, 2)); 43 | }); 44 | } 45 | 46 | /** 47 | * Called when the signed in status changes, to update the UI 48 | * appropriately. After a sign-in, the API is called. 49 | */ 50 | function updateSigninStatus (isSignedIn) { 51 | if (isSignedIn) { 52 | authorizeButton.style.display = 'none'; 53 | signoutButton.style.display = 'block'; 54 | listFiles(function (fs) { 55 | console.log(fs); 56 | }); 57 | $('.authorize-only').css('display', ''); 58 | } else { 59 | authorizeButton.style.display = 'block'; 60 | signoutButton.style.display = 'none'; 61 | $('.authorize-only').css('display', 'none'); 62 | } 63 | } 64 | 65 | /** 66 | * Sign in the user upon button click. 67 | */ 68 | function handleAuthClick (event) { 69 | gapi.auth2.getAuthInstance().signIn(); 70 | } 71 | 72 | /** 73 | * Sign out the user upon button click. 74 | */ 75 | function handleSignoutClick (event) { 76 | gapi.auth2.getAuthInstance().signOut(); 77 | } 78 | 79 | /** 80 | * Append a pre element to the body containing the given message 81 | * as its text node. Used to display the results of the API call. 82 | * 83 | * @param {string} message Text to be placed in pre element. 84 | */ 85 | function appendPre (message) { 86 | console.log(message); 87 | // var pre = document.getElementById('content'); 88 | // var textContent = document.createTextNode(message + '\n'); 89 | // pre.appendChild(textContent); 90 | } 91 | 92 | /** 93 | * Print files. 94 | */ 95 | function listFiles (filesFun, errorFun) { 96 | gapi.client.drive.files.list({ 97 | spaces: 'appDataFolder', 98 | // 'q':"'appDataFolder' in parents", 99 | // 'pageSize': 10, 100 | fields: 'files(id, name, parents,createdTime,modifiedTime)' 101 | }).then(function (response) { 102 | filesFun(response.result.files); 103 | }); 104 | } 105 | 106 | function createFolder (name, parents = null) { 107 | if (parents === null) { 108 | parents = ['appDataFolder']; 109 | } 110 | gapi.client.drive.files.create({ 111 | resource: { 112 | name: name, 113 | parents: parents, 114 | mimeType: 'application/vnd.google-apps.folder' 115 | }, 116 | fields: 'id' 117 | }).then(function (err, file) { 118 | if (err) { 119 | // Handle error 120 | console.error(err); 121 | } else { 122 | console.log('Folder Id: ', file.id); 123 | } 124 | }); 125 | } 126 | 127 | function getFileContent (fileId, resultFun) { 128 | gapi.client.drive.files.get({ 129 | fileId: fileId, 130 | alt: 'media' 131 | }).then(resultFun); 132 | } 133 | 134 | // https://gist.github.com/tanaikech/bd53b366aedef70e35a35f449c51eced 135 | function uploadFile (name, content) { 136 | const file = new Blob([JSON.stringify(content)], { type: 'application/json' }); 137 | const metadata = { 138 | name: name, // Filename at Google Drive 139 | mimeType: 'application/json', // mimeType at Google Drive 140 | parents: ['appDataFolder'] // Folder ID at Google Drive 141 | }; 142 | const accessToken = gapi.auth.getToken().access_token; // Here gapi is used for retrieving the access token. 143 | const form = new FormData(); 144 | form.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' })); 145 | form.append('file', file); 146 | 147 | return fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', { 148 | method: 'POST', 149 | headers: new Headers({ Authorization: 'Bearer ' + accessToken }), 150 | body: form 151 | }) 152 | // .then((res) => { 153 | // return res.json(); 154 | // }).then(function (val) { 155 | // console.log(val); 156 | // }); 157 | } 158 | 159 | // ::: :::::::: :::::::: ::: ::: 160 | // :+: :+: :+: :+: :+: :+: :+: :+: 161 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ 162 | // +#+ +#+ +:+ +#+ +#++:++#++: +#+ 163 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ 164 | // #+# #+# #+# #+# #+# #+# #+# #+# 165 | // ########## ######## ######## ### ### ########## 166 | 167 | let __GDRIVE_saveFilename = null; 168 | let __GDRIVE_savedID = null; 169 | let __files = new Map(); 170 | let __files_allInfo = new Map(); 171 | 172 | function fillFilesList (rowClickFun) { 173 | listFiles(function (files) { 174 | __files = new Map(); 175 | __files_allInfo = new Map(); 176 | files.forEach((file) => { 177 | __files.set(file.name, file.id); 178 | __files_allInfo.set(file.name, file) 179 | console.log(file); 180 | 181 | const row = document.createElement('div'); 182 | row.className = 'row my-1'; 183 | row.dataset.fileId = file.id; 184 | row.dataset.name = file.name; 185 | 186 | const col_name = document.createElement('div'); 187 | col_name.className = 'col-10 btn btn-outline-secondary'; 188 | col_name.innerText = file.name; 189 | col_name.onclick = (function (_row) { 190 | console.log('row click'); 191 | return function(){rowClickFun(_row); 192 | }}(row)); 193 | 194 | let col_del = document.createElement('div'); 195 | col_del.className = 'col'; 196 | let del_btn = document.createElement('div'); 197 | del_btn.className = 'btn btn-danger align-self-end'; 198 | del_btn.innerHTML = '⌫'; 199 | del_btn.title = 'delete ' + file.name; 200 | del_btn.onclick = (function (_row) { 201 | return function () { 202 | _('#modalYesNoLabel').innerHTML = 'Delete?'; 203 | _('#modalYesNoBody').innerHTML = 'Really delete ' + _row.dataset.name + '?'; 204 | _('#modalYesNo-Yes').onclick = function () { 205 | gapi.client.drive.files.delete({ 206 | fileId: _row.dataset.fileId 207 | }).then(function (a) { 208 | console.log(a); 209 | if (a.status === 204) { 210 | _row.remove(); 211 | __files.delete(_row.dataset.name); 212 | } 213 | }); 214 | }; 215 | $('#modalYesNo').modal('show'); 216 | }; 217 | })(row); 218 | col_del.appendChild(del_btn); 219 | 220 | row.appendChild(col_name); 221 | row.appendChild(col_del); 222 | 223 | _('#modal-list').appendChild(row); 224 | }); 225 | }); 226 | } 227 | 228 | _('#load_gdrive').addEventListener('click', function () { 229 | console.log('GDrive load..'); 230 | 231 | _('#modal-input').value = ''; 232 | _('#modal-input').oninput = function () { 233 | if (__files.has(_('#modal-input').value)) { 234 | _('#modal-save').style.display = ''; 235 | } else { 236 | _('#modal-save').style.display = 'none'; 237 | } 238 | }; 239 | 240 | _('#modal-list').innerHTML = ''; 241 | _('#exampleModalLabel').innerHTML = 'Load from Google Drive file:'; 242 | _('#modal-save').innerHTML = 'Save'; 243 | _('#modal-save').style.display = 'none'; 244 | 245 | fillFilesList((row) => { 246 | console.log(row); 247 | loadFromGDriveFile(row.dataset.name, row.dataset.fileId); 248 | }); 249 | }); 250 | 251 | function loadFromGDriveFile(name, fileId){ 252 | if(fileId==undefined){ 253 | fileId = __files.get(name); 254 | } 255 | getFileContent(fileId, function (e) { 256 | if (e.status === 200) { 257 | // file loaded OK, load nodes'n'stuff 258 | loadFromG(JSON.parse(e.result)); 259 | // only hide on OK load 260 | $('#exampleModal').modal('hide'); 261 | // save Filename for faster save 262 | __GDRIVE_saveFilename = name; 263 | __GDRIVE_savedID = fileId; 264 | }else { 265 | alert('error.. ' + e); 266 | console.log(e); 267 | } 268 | }); 269 | } 270 | 271 | function saveToGDrive (filename) { 272 | uploadFile( 273 | filename, 274 | JSON.stringify(saveToG()) 275 | ).then((response) => { 276 | return response.json(); 277 | }) 278 | .then((data) => { 279 | console.log('Saved to GDRIVE'); 280 | // TODO: simple mobile-like notification 281 | console.log(data); 282 | __GDRIVE_savedID = data.id; 283 | localStorage.setItem('__GDRIVE_savedID', __GDRIVE_savedID); 284 | }); 285 | // save filename 286 | __GDRIVE_saveFilename = filename; 287 | localStorage.setItem('__GDRIVE_saveFilename', __GDRIVE_saveFilename); 288 | } 289 | 290 | function gdriveRewrite(filename, id){ 291 | // I was not able to rewrite file content 292 | // , so I will just delete and save 293 | gapi.client.drive.files.delete({ 294 | fileId: id 295 | }).then(function (a) { 296 | if (a.status == 204) { 297 | // Now save 298 | saveToGDrive(filename); 299 | $('#exampleModal').modal('hide'); 300 | toast(filename + ' successfully written to GDrive'); 301 | } else { 302 | console.log(a); 303 | alert('Error while rewriting...'); 304 | } 305 | }); 306 | } 307 | 308 | _('#saveas_gdrive').addEventListener('click', function () { 309 | console.log('GDrive save as..'); 310 | 311 | _('#modal-input').value = __GDRIVE_saveFilename || defaultFilename(); 312 | _('#modal-input').oninput = function () {}; 313 | _('#modal-save').style.display = ''; 314 | _('#exampleModalLabel').innerHTML = 'Save to Google Drive file:'; 315 | _('#modal-save').innerHTML = 'Save'; 316 | _('#modal-list').innerHTML = ''; 317 | 318 | _('#modal-save').onclick = function () { 319 | saveToGDrive(_('#modal-input').value); 320 | }; 321 | 322 | fillFilesList((_row) => { 323 | showModalYesNo( 324 | 'Overwrite?', 325 | 'Really Overwrite ' + _row.dataset.name + '?', 326 | function () { 327 | gdriveRewrite(_row.dataset.name, _row.dataset.fileId); 328 | }); 329 | }); 330 | }, false); 331 | 332 | _('#save_gdrive').addEventListener('click', function () { 333 | console.log('GDrive save..'); 334 | 335 | if ( __GDRIVE_savedID !== null ) { 336 | gdriveRewrite(__GDRIVE_saveFilename, __GDRIVE_savedID); 337 | } else { 338 | _('#saveas_gdrive').click(); 339 | } 340 | 341 | }, false); 342 | 343 | 344 | 345 | 346 | __GDRIVE_saveFilename = localStorage.getItem('__GDRIVE_saveFilename'); 347 | __GDRIVE_savedID = localStorage.getItem('__GDRIVE_savedID'); 348 | 349 | if(__GDRIVE_saveFilename){ 350 | 351 | listFiles((files) => { 352 | log("gdive file list loaded") 353 | for(var file of files){ 354 | if(file.name == __GDRIVE_saveFilename){ 355 | if(file.id == __GDRIVE_savedID){ 356 | log("GDrive has the same file") 357 | } else { 358 | showModalYesNo( 359 | 'Load from Google?', 360 | 'Looks like you have worked on ' + __GDRIVE_saveFilename + ' GDrive file, but its version on Google is different from yours, load from Google (It will overwrite local changes)?', 361 | function () { 362 | loadFromGDriveFile(__GDRIVE_saveFilename, file.id) 363 | } 364 | ); 365 | } 366 | } 367 | } 368 | }) 369 | 370 | } 371 | -------------------------------------------------------------------------------- /nodes.js: -------------------------------------------------------------------------------- 1 | 2 | let _NODES = []; 3 | let _DOMId2node = new Map(); 4 | let _DOMId2nodej = new Map(); 5 | 6 | let _NODEId2node = new Map(); 7 | 8 | function gen_DOMId2nodej () { 9 | _DOMId2node = new Map(); 10 | _DOMId2nodej = new Map(); 11 | _NODEId2node = new Map(); 12 | 13 | for (let j = 0; j < _NODES.length; j++) { 14 | _DOMId2node.set(_NODES[j].dom.id, _NODES[j]); 15 | _DOMId2nodej.set(_NODES[j].dom.id, j); 16 | _NODEId2node.set(_NODES[j].id, _NODES[j].id); 17 | } 18 | } 19 | 20 | function stripNode (d) { 21 | // strips only relevant data for nodes, also convert to numerical 22 | return { 23 | id: ('id' in d) ? d.id+'' : newNodeID(), // newNodeID(),// 24 | x: 1 * d.x, 25 | y: 1 * d.y, 26 | fontSize: 1 * d.fontSize, 27 | text: d.text, 28 | rotate: 'rotate' in d ? 1 * d.rotate:0, 29 | deleted: 'deleted' in d ? d.deleted : false, 30 | style: 'style' in d ? delete_defaults(d.style, default_node_style) : undefined 31 | }; 32 | } 33 | 34 | function isNode (obj) { 35 | return 'x' in obj; // yeah.. 36 | } 37 | 38 | function idNode (id) { 39 | return _NODEId2node.get(id); 40 | } 41 | 42 | function domNode (dom) { 43 | return _DOMId2node.get(typeof (dom) === 'string' ? dom : dom.id); 44 | } 45 | 46 | function newNodeID (id = null) { 47 | return newID(id || 'n', idNode); 48 | } 49 | 50 | function pushNodeNDom(node, tdom) { 51 | console.log("pushing.."); 52 | _NODES.push(node); 53 | _NODEId2node.set(node.id, node); 54 | _DOMId2node.set(tdom.id, node); 55 | _DOMId2nodej.set(tdom.id, _NODES.length - 1); 56 | } 57 | 58 | 59 | function deleteNode(d) { 60 | if (isDom(d)) { 61 | d = domNode(d); 62 | } 63 | // _NODES 64 | // ixs (TODO:optimize further) 65 | gen_DOMId2nodej(); 66 | // remove from _NODES 67 | _NODES.splice(_DOMId2nodej.get(d.dom.id), 1); 68 | // remove from index 69 | _DOMId2node.delete(d.dom.id); 70 | // remove from DOM 71 | node_container.removeChild(d.dom); 72 | // remove from saved 73 | _localStorage.removeNode(d); 74 | // removeNodeFromLocalStorage(d); 75 | } 76 | 77 | // :::: ::: :::::::::: ::: ::: :::: ::: :::::::: ::::::::: :::::::::: 78 | // :+:+: :+: :+: :+: :+: :+:+: :+: :+: :+: :+: :+: :+: 79 | // :+:+:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ 80 | // +#+ +:+ +#+ +#++:++# +#+ +:+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +:+ +#++:++# 81 | // +#+ +#+#+# +#+ +#+ +#+#+ +#+ +#+ +#+#+# +#+ +#+ +#+ +#+ +#+ 82 | // #+# #+#+# #+# #+#+# #+#+# #+# #+#+# #+# #+# #+# #+# #+# 83 | // ### #### ########## ### ### ### #### ######## ######### ########## 84 | 85 | function newNode (node, redraw=true, addToNodes=true) { 86 | var tdom; 87 | // console.log(d); 88 | if ('className' in node) { 89 | console.log('newNode with DOM node provided:'); 90 | tdom = node; 91 | node = domNode(tdom); 92 | } else { 93 | console.log('newNode with node provided'); 94 | 95 | fillMissingNodeFields(node); 96 | 97 | tdom = _ce('div' 98 | , 'id', 'node_' + node.id 99 | , 'className', 'node ui-rotatable' 100 | , 'onclick', onNodeClick 101 | , 'ondblclick', onNodeDblClick 102 | , 'onmousedown', onNodeMouseDown 103 | , 'style.display', 'none' 104 | ); 105 | 106 | if(addToNodes){ 107 | pushNodeNDom(node, tdom); 108 | } 109 | } 110 | tdom.innerHTML = ''; 111 | 112 | const tcontent = _ce('div' 113 | , 'className', 'np-n-c' 114 | , 'innerHTML', getHTML(node) 115 | ); 116 | 117 | if(node.is_svg){ 118 | // tcontent.classList.add('svg'); 119 | tdom.classList.add('svg'); 120 | 121 | node.svg_dom = tcontent.getElementsByTagName('svg')[0]; 122 | 123 | node.path_dom = node.svg_dom.getElementsByTagName('path')[0]; 124 | 125 | node.path_dom.setAttribute("stroke", node.style.color); 126 | node.path_dom.setAttribute("strokeWidth", node.style.strokeWidth); 127 | node.path_dom.setAttribute("fill", node.style.fill); 128 | 129 | 130 | node.svg_dom.onmousedown = function(e) { 131 | //if(onNodeMouseDown.path_ok) 132 | } 133 | node.path_dom.onmousedown = function (e) { 134 | onNodeMouseDown.path_ok = true; 135 | } 136 | 137 | node.path_dom.ondblclick = function (e) { 138 | onNodeDblClick.path_ok = true; 139 | } 140 | node.path_dom.onclick = function (e) { 141 | onNodeClick.path_ok = true; 142 | } 143 | 144 | } 145 | if(node.is_img){ 146 | tdom.classList.add('img'); 147 | 148 | node.img_dom = tcontent.getElementsByTagName('img')[0]; 149 | node.img_dom.onload = function(e) { 150 | console.log('img load'); 151 | console.log(this); 152 | } 153 | node.img_dom.addEventListener('load',function () { 154 | console.log('node '+node.id+' img ready!!'); 155 | node.size = [node.img_dom.width, node.img_dom.height]; 156 | }); 157 | } 158 | 159 | // Tooltip 160 | 161 | const tt = _ce('div' 162 | , 'className', 'position-absolute start-0 np-n-tooltip'// translate-middle start-50 163 | ); 164 | 165 | if (node.is_img == false) { 166 | // not entirely image-based node, add color select 167 | const tcolorselect = _ce('input' 168 | , 'type', 'color' 169 | , 'value', node.style.color 170 | , 'oninput', function (e) { 171 | node.style.color = this.value; 172 | node.content_dom.style.color = this.value; 173 | if (node.is_svg) { 174 | node.path_dom.setAttribute("stroke", this.value); 175 | } 176 | } 177 | , 'onchange', function (e) { 178 | applyAction( { 179 | type: 'E', 180 | node_ids: [ node.id ], 181 | oldValues: [ { 'style.color': this.dataset['oldValue'] } ], 182 | newValues: [ { 'style.color': this.value } ] 183 | }) 184 | } 185 | ); 186 | tcolorselect.dataset['oldValue'] = node.style.color; 187 | tt.appendChild(tcolorselect); 188 | 189 | } 190 | if((node.is_img == false) && (node.is_svg == false)){ 191 | if (tcontent.innerHTML.indexOf('= 0) { 192 | // text align only valuable for multiline nodes 193 | ['left', 'center', 'right'].forEach((jta) => { 194 | let tbtn = _ce('button' 195 | , 'className', 'np-n-t-btn np-n-t-ta' + (node.style.textAlign == jta ? ' np-n-t-ta-selected':'') 196 | , 'innerHTML', '' 197 | , 'onclick', function (e) { 198 | console.log('clicked on text-align=' + this.dataset.textAlign); 199 | $(this.parent) 200 | .find('.np-n-t-ta') 201 | .removeClass('np-n-t-ta-selected') 202 | .find('[data-text-align="' + this.dataset.textAlign + '"]') 203 | .addClass('np-n-t-ta-selected'); 204 | 205 | applyAction({ 206 | type: 'E', 207 | node_ids: [ node.id ], 208 | newValues: [ { 'style.textAlign': this.dataset.textAlign } ] 209 | }) 210 | // node.style.textAlign = this.dataset.textAlign; 211 | node.content_dom.style.textAlign = this.dataset.textAlign; 212 | // newNode(node.node); 213 | // selectNode(node) 214 | e.stopPropagation(); 215 | } 216 | ); 217 | 218 | tbtn.dataset.textAlign = jta; 219 | tt.appendChild(tbtn); 220 | }); 221 | } 222 | } 223 | 224 | const tplusbtn = _ce('button' 225 | , 'className', 'np-n-t-btn plus-button'// btn btn-outline-primary' 226 | , 'onclick', function (e) { editFontSize(+1); e.stopPropagation(); } 227 | , 'title', 'make BIGGER' 228 | , 'innerHTML', '' 229 | ); 230 | const tminusbtn = _ce('button' 231 | , 'className', 'np-n-t-btn minus-button'// 'btn btn-outline-primary' 232 | , 'onclick', function (e) { editFontSize(-1); e.stopPropagation(); } 233 | , 'title', 'Make smaller' 234 | , 'innerHTML', '' 235 | ); 236 | tt.appendChild(tplusbtn); 237 | tt.appendChild(tminusbtn); 238 | 239 | if (node.is_svg) { 240 | // add line width !! 241 | const tplusbtnLW = _ce('button' 242 | , 'className', 'np-n-t-btn plus-button'// btn btn-outline-primary' 243 | , 'onclick', function (e) { changeStrokeWidth(+1); e.stopPropagation(); } 244 | , 'title', 'make thicker' 245 | , 'innerHTML', '
➖' 246 | ); 247 | const tminusbtnLW = _ce('button' 248 | , 'className', 'np-n-t-btn minus-button'// 'btn btn-outline-primary' 249 | , 'onclick', function (e) { changeStrokeWidth(-1); e.stopPropagation(); } 250 | , 'title', 'make thinner' 251 | , 'innerHTML', '
—' 252 | ); 253 | tt.appendChild(tplusbtnLW ); 254 | tt.appendChild(tminusbtnLW ); 255 | 256 | // fill? 257 | const tbutton = _ce('button' 258 | ,'className', "np-n-t-btn" 259 | ); 260 | 261 | const tlabel = _ce('label' 262 | ,'innerHTML', node.style.fill=='none'?'':'' 263 | ,'ondblclick', function (e) { 264 | applyAction({ 265 | type: 'E', 266 | node_ids: [node.id], 267 | oldValues: [{'style.fill': this.dataset['oldValue']}], 268 | newValues: [{'style.fill': 'none'}] 269 | }); 270 | e.stopPropagation(); 271 | } 272 | ); 273 | tlabel.setAttribute('for','inputfill_'+node.id); 274 | if(node.style.fill!=='none'){ 275 | tlabel.style.color = node.style.fill; 276 | } 277 | 278 | 279 | const tfillinput = _ce('input' 280 | ,'type','color' 281 | ,'value', node.style.fill=='none'?'#ffffff':node.style.fill 282 | ,'id','inputfill_'+node.id 283 | ,'style','width:0px;height:0px;' 284 | // ,'hidden','hidden' 285 | ,'oninput', function (e) { 286 | tlabel.style.color = this.value; 287 | node.style.fill = this.value; 288 | node.path_dom.setAttribute('fill', this.value); 289 | tlabel.innerHTML = ''; 290 | } 291 | ,'onchange', function (e) { 292 | applyAction({ 293 | type: 'E', 294 | node_ids: [node.id], 295 | oldValues: [{'style.fill': this.dataset['oldValue']}], 296 | newValues: [{'style.fill': this.value}], 297 | }); 298 | } 299 | 300 | ) 301 | tfillinput.dataset['oldValue'] = node.style.fill; 302 | tbutton.appendChild(tlabel); 303 | tbutton.appendChild(tfillinput); 304 | 305 | // tt.appendChild(tfillinput); 306 | tt.appendChild(tbutton); 307 | } 308 | 309 | 310 | tt.addEventListener('click', function(e) { 311 | e.stopPropagation(); 312 | }) 313 | tt.addEventListener('mousedown', function(e) { 314 | e.stopPropagation(); 315 | }) 316 | tt.addEventListener('mouseup', function(e) { 317 | e.stopPropagation(); 318 | }) 319 | tt.addEventListener('dblclick', function (e) { 320 | e.stopPropagation(); 321 | }); 322 | 323 | tdom.appendChild(tt); 324 | 325 | for (const p of Object.keys(node.style)) { 326 | tcontent.style[p] = node.style[p]; 327 | } 328 | 329 | tdom.appendChild(tcontent); 330 | 331 | node.dom = tdom; 332 | node.content_dom = tcontent; 333 | 334 | if (!('className' in node)) { 335 | node_container.appendChild(tdom); 336 | } 337 | 338 | 339 | 340 | tdom.getElementsByTagName('a').forEach( (elt) => { 341 | if (elt.href) { 342 | elt.onclick = (e) => { 343 | e.stopPropagation(); 344 | }; 345 | elt.onmousedown = (e) => { 346 | e.stopPropagation(); 347 | }; 348 | } 349 | }); 350 | 351 | if (redraw) { 352 | redrawNode(node); 353 | } else { 354 | updateNode(node); 355 | } 356 | 357 | if(tdom.classList.contains('selected')){ 358 | // deselectOneDOM(tdom); 359 | try{$(tdom).rotatable('destroy');}catch(e){} 360 | setTimeout(function(){selectOneDOM(tdom);},1); 361 | } 362 | 363 | return tdom; 364 | } 365 | 366 | function fillMissingNodeFields(node){ 367 | if ((!('id' in node)) || (node.id === undefined) || (idNode(node.id))) { 368 | node.id = newNodeID(); 369 | } 370 | if (!('rotate' in node)) { 371 | node.rotate = 0; 372 | } 373 | if (!('x' in node)) { 374 | let mousePos = _Mouse.pos; 375 | if ('mousePos' in node) { 376 | mousePos = node.mousePos; 377 | } 378 | const mouseXY = _View.clientToPos(mousePos); 379 | node.x = mouseXY[0]; 380 | node.y = mouseXY[1]; 381 | } 382 | if (!('fontSize' in node)) { 383 | node.fontSize = 20 / _View.state.S; 384 | } 385 | if ((!node.hasOwnProperty('style'))|| 386 | (node.style === undefined)) { 387 | node.style = Object.assign({}, default_node_style); 388 | } else { 389 | node.style = Object.assign({}, default_node_style, node.style); 390 | } 391 | } 392 | 393 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | /* margin:0px; 3 | height:100%; 4 | width:100%; 5 | position:fixed; */ 6 | overflow: hidden; /* Hide scrollbars */ 7 | font-family: 'Open Sans'; 8 | touch-action: none; 9 | } 10 | 11 | #wrapper { 12 | /* overflow-x: hidden; */ 13 | width: 100%; 14 | } 15 | 16 | #navbar{ 17 | z-index: 900; 18 | } 19 | 20 | #sidebar-wrapper { 21 | min-height: 100vh; 22 | margin-left: -15rem; 23 | -webkit-transition: margin .25s ease-out; 24 | -moz-transition: margin .25s ease-out; 25 | -o-transition: margin .25s ease-out; 26 | transition: margin .25s ease-out; 27 | z-index: 500; 28 | } 29 | 30 | #sidebar-wrapper .sidebar-heading { 31 | padding: 0.875rem 1.25rem; 32 | font-size: 1.2rem; 33 | } 34 | 35 | #sidebar-wrapper ul { 36 | width: 15rem; 37 | } 38 | 39 | #page-content-wrapper { 40 | min-width: 100vw; 41 | overflow: hidden; 42 | } 43 | 44 | #wrapper.toggled #sidebar-wrapper { 45 | margin-left: 0; 46 | } 47 | 48 | @media (min-width: 768px) { 49 | #sidebar-wrapper { 50 | margin-left: 0; 51 | } 52 | 53 | #page-content-wrapper { 54 | min-width: 0; 55 | width: 100%; 56 | } 57 | 58 | #wrapper.toggled #sidebar-wrapper { 59 | margin-left: -15rem; 60 | } 61 | } 62 | 63 | 64 | div.node{ 65 | display: inline-block; 66 | width:auto; 67 | 68 | } 69 | 70 | p{ 71 | margin:0px; 72 | } 73 | 74 | .dot circle { 75 | fill: lightsteelblue; 76 | stroke: steelblue; 77 | stroke-width: 1.5px; 78 | } 79 | 80 | .dot circle.dragging { 81 | fill: red; 82 | stroke: brown; 83 | } 84 | 85 | .axis line { 86 | fill: none; 87 | stroke: #ddd; 88 | shape-rendering: crispEdges; 89 | vector-effect: non-scaling-stroke; 90 | } 91 | 92 | .node{ 93 | /* width: 100%; 94 | height: 100%; 95 | text-align: left; */ 96 | position: absolute; 97 | width:auto; 98 | /* text-align: left; */ 99 | /* overflow: hidden; */ 100 | white-space:nowrap; 101 | /* -webkit-user-modify: read-write; 102 | overflow-wrap: break-word; 103 | -webkit-line-break: after-white-space; */ 104 | 105 | font-size: inherit; 106 | /* transition-timing-function: linear; */ 107 | } 108 | 109 | 110 | .node svg{ 111 | position: absolute; 112 | left:0px; 113 | top:0px; 114 | /* transition-timing-function: linear; */ 115 | /* transition-duration: inherit; */ 116 | } 117 | 118 | .node svg path { 119 | cursor: pointer; 120 | } 121 | .node .np-n-c.img{ 122 | transition-duration: inherit; 123 | } 124 | /* .node img{ 125 | transition-duration: inherit; 126 | 127 | } */ 128 | 129 | .node h1{ 130 | font-size: 1.5em; 131 | font-weight: bold; 132 | } 133 | .node h2{ 134 | font-size: 1.4em; 135 | font-weight: bold; 136 | } 137 | .node h3{ 138 | font-size: 1.35em; 139 | font-weight: bold; 140 | } 141 | .node h4{ 142 | font-size: 1.3em; 143 | } 144 | .node h5{ 145 | font-size: 1.25em; 146 | font-style:italic; 147 | } 148 | .node h6{ 149 | font-size: 1.1em; 150 | font-style:oblique; 151 | } 152 | 153 | 154 | .node.selected{ 155 | /* fill:red; 156 | color:red; */ 157 | border:1px solid black; 158 | z-index: 100; 159 | } 160 | 161 | .node blockquote{ 162 | margin-inline-start: 1em; 163 | margin-inline-end: 0px; 164 | } 165 | .node ol,ul{ 166 | padding-inline-start: 1em; 167 | padding-inline-start: 1em; 168 | } 169 | 170 | /* .inside_node{ 171 | 172 | } */ 173 | #container{ 174 | 175 | width: 100%; 176 | height:100%; 177 | } 178 | #container.drag-hover{ 179 | background-color: #aaa; 180 | } 181 | 182 | #text{ 183 | resize: none; 184 | /* overflow: hidden; */ 185 | min-height: 50px; 186 | max-height: 100px; 187 | } 188 | 189 | .custom-file-upload { 190 | border: 1px solid #ccc; 191 | display: inline-block; 192 | padding: 6px 12px; 193 | cursor: pointer; 194 | } 195 | input[type="file"] { 196 | display: none; 197 | } 198 | 199 | 200 | /* 201 | ===================================================================§§ 202 | */ 203 | 204 | .notification-top { 205 | position: fixed; 206 | width: 100%; 207 | top: 0; 208 | left: 0; 209 | /* Rest of your styling */ 210 | border: 1px solid black; 211 | background-color: white; 212 | z-index: 1000; 213 | } 214 | .notification-bottom { 215 | position: fixed; 216 | width: 100%; 217 | bottom: 0; 218 | left: 0; 219 | /* Rest of your styling */ 220 | border: 1px solid black; 221 | background-color: white; 222 | z-index: 1000; 223 | } 224 | 225 | #node_container{ 226 | /* transition-duration: 0.2s; */ 227 | display: block; 228 | position: absolute; 229 | } 230 | 231 | .node{ 232 | /* display: block; */ 233 | /* white-space: pre; */ 234 | position: absolute; 235 | /* transition-duration: 0.2s; */ 236 | user-select: none; /* CSS3 (little to no support) */ 237 | -ms-user-select: none; /* IE 10+ */ 238 | -moz-user-select: none; /* Gecko (Firefox) */ 239 | -webkit-user-select: none; /* Webkit (Safari, Chrome) */ 240 | } 241 | .node.zoom{ 242 | transition-duration: 0.2s; 243 | } 244 | 245 | a.node_a{ 246 | color: inherit; /* blue colors for links too */ 247 | text-decoration: inherit; /* no underline */ 248 | } 249 | 250 | a.local{ 251 | cursor:pointer; 252 | } 253 | 254 | .ui-rotatable-handle{ 255 | width: 20px; 256 | height: 20px; 257 | position: absolute; 258 | left:-20px; 259 | bottom:-20px; 260 | /* border-radius: 6px; */ 261 | /* border-width: 1.5px; */ 262 | /* border-style: solid; */ 263 | /* border-color: rgb(0,0,0); */ 264 | 265 | content: url(img/r.svg); 266 | 267 | z-index: 10000; 268 | 269 | cursor:grab; 270 | } 271 | .ui-rotatable-disabled .ui-rotatable-handle{ 272 | display: none; 273 | } 274 | 275 | .node img{ 276 | transition-duration:inherit; 277 | -webkit-user-drag: none; 278 | -khtml-user-drag: none; 279 | -moz-user-drag: none; 280 | -o-user-drag: none; 281 | user-drag: none; 282 | } 283 | 284 | #select-box{ 285 | position: absolute; 286 | border: 1px solid black; 287 | z-index: 10000; 288 | display: none; 289 | } 290 | 291 | #copydiv{ 292 | position: absolute; 293 | left:-10000px; 294 | top:-100000px; 295 | opacity: 0; 296 | } 297 | 298 | 299 | .horizontal-scrollable > .row { 300 | overflow-x: auto; 301 | white-space: nowrap; 302 | -webkit-overflow-scrolling: touch; 303 | } 304 | 305 | .horizontal-scrollable > .row > .col { 306 | display: inline-block; 307 | float: none; 308 | } 309 | 310 | 311 | 312 | /* https://getbootstrap.com/docs/5.0/examples/sidebars/# */ 313 | body { 314 | display: flex; 315 | flex-wrap: nowrap; 316 | height: 100vh; 317 | height: -webkit-fill-available; 318 | overflow-x: hidden; 319 | overflow-y: hidden; 320 | } 321 | body > * { 322 | flex-shrink: 0; 323 | min-height: -webkit-fill-available; 324 | } 325 | 326 | .b-example-divider { 327 | width: 1.5rem; 328 | height: 100%; 329 | background-color: rgba(0, 0, 0, .1); 330 | border: solid rgba(0, 0, 0, .15); 331 | border-width: 1px 0; 332 | box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); 333 | } 334 | 335 | .bi { 336 | vertical-align: -.125em; 337 | pointer-events: none; 338 | fill: currentColor; 339 | } 340 | 341 | .dropdown-toggle { outline: 0; } 342 | 343 | .nav-flush .nav-link { 344 | border-radius: 0; 345 | } 346 | 347 | .btn-toggle { 348 | display: inline-flex; 349 | align-items: center; 350 | padding: .25rem .5rem; 351 | font-weight: 600; 352 | color: rgba(0, 0, 0, .65); 353 | background-color: transparent; 354 | border: 0; 355 | } 356 | .btn-toggle:hover, 357 | .btn-toggle:focus { 358 | color: rgba(0, 0, 0, .85); 359 | background-color: #d2f4ea; 360 | } 361 | 362 | .btn-toggle::before { 363 | width: 1.25em; 364 | line-height: 0; 365 | content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); 366 | transition: transform .35s ease; 367 | transform-origin: .5em 50%; 368 | } 369 | 370 | .btn-toggle[aria-expanded="true"] { 371 | color: rgba(0, 0, 0, .85); 372 | } 373 | .btn-toggle[aria-expanded="true"]::before { 374 | transform: rotate(90deg); 375 | } 376 | 377 | .btn-toggle-nav a { 378 | display: inline-flex; 379 | padding: .375rem .5rem; 380 | margin-top: 0px; 381 | margin-left: 1.25rem; 382 | text-decoration: none; 383 | } 384 | .btn-toggle-nav a:hover, 385 | .btn-toggle-nav a:focus { 386 | background-color: #d2f4ea; 387 | } 388 | 389 | .scrollarea { 390 | overflow-y: auto; 391 | } 392 | 393 | .fw-semibold { font-weight: 600; } 394 | .lh-tight { line-height: 1.25; } 395 | 396 | /* ----- */ 397 | 398 | #modal-list{ 399 | max-height: 60vh; 400 | overflow-y: scroll; 401 | } 402 | 403 | /* ------------------------------- */ 404 | #np-places-header button{ 405 | padding: 0.4rem; 406 | } 407 | 408 | .places-place{ 409 | max-width: 8em; 410 | overflow: hidden; 411 | } 412 | .places-folder{ 413 | max-width: 8em; 414 | overflow: hidden; 415 | } 416 | 417 | 418 | /* -------------- */ 419 | 420 | #searchSideBar{ 421 | width: 18rem; 422 | transition-duration: 0.25s; 423 | z-index: 900; 424 | } 425 | 426 | #searchSideBar.toggled{ 427 | margin-right:-18rem; 428 | } 429 | 430 | #search-toggle{ 431 | position: relative; 432 | margin-left: -32px; 433 | width:32px; 434 | border-radius: 48%; 435 | } 436 | 437 | #searchResultContainer{ 438 | overflow-y: auto; 439 | } 440 | 441 | .np-sr-row .col{ 442 | text-overflow: ellipsis; 443 | white-space:nowrap; 444 | overflow: hidden; 445 | max-height: 3rem; 446 | } 447 | 448 | .np-sr-row .bi-folder-symlink{ 449 | font-size: 180%; 450 | border-radius: 0.4rem; 451 | padding: 0.2rem; 452 | } 453 | 454 | /* .np-sr-row .bi-folder-symlink:hover{ 455 | background-color: black; 456 | color:white; 457 | } */ 458 | 459 | .node.np-search-preview{ 460 | border: 2px dotted orange; 461 | } 462 | 463 | .np-search-place-preview{ 464 | background-color: orange; 465 | } 466 | 467 | /*----------------*/ 468 | 469 | /* 470 | .np-n-c{ 471 | } 472 | */ 473 | 474 | .np-n-tooltip{ 475 | display: none; 476 | height:3rem; 477 | font-size: 1rem; 478 | margin-top: -3rem; 479 | border-radius: 0.3rem; 480 | /* border:1px solid black; */ 481 | vertical-align: middle; 482 | text-align: center; 483 | padding: 0.125rem; 484 | 485 | } 486 | .selected .np-n-tooltip{ 487 | display:flex; 488 | align-items: center; 489 | /* transition-duration: 0.5s; */ 490 | } 491 | 492 | .np-n-t-btn{ 493 | /*--*/ 494 | background-color: #fff; 495 | height: 2em; 496 | width: 2em; 497 | border: 2px solid #ccf; 498 | border-radius: 999px; 499 | position: relative; 500 | vertical-align: middle; 501 | margin-right: 0.33em; 502 | } 503 | .np-n-t-btn:hover,.np-n-btn:focus{ 504 | /* background-color:#ccc */ 505 | background-color: #eef; 506 | } 507 | 508 | .np-n-tooltip input{ 509 | height: 1.3em; 510 | width: 1.3em; 511 | margin: 0.1em; 512 | } 513 | 514 | .np-n-t-ta:hover{ 515 | background-color: #eef; 516 | } 517 | 518 | .np-n-t-ta-selected{ 519 | /* */ 520 | background-color: #ccf; 521 | } 522 | 523 | 524 | input[type="color"]{ 525 | background-color:white; 526 | border:none; 527 | margin-right: 0.2em; 528 | width:1.8em; 529 | height:1.8em; 530 | padding:0; 531 | margin-right: 0.33em; 532 | } 533 | /* -webkit */ 534 | input[type="color"]::-webkit-color-swatch-wrapper { 535 | padding: 0; 536 | } 537 | 538 | input[type="color"]::-webkit-color-swatch { 539 | border: none; 540 | border-radius: 500%; 541 | } 542 | /* firefox */ 543 | input[type=color]::-moz-focus-inner { 544 | border: none; 545 | padding: 0; 546 | border-radius: 500%; 547 | } 548 | 549 | input[type=color]::-moz-color-swatch { 550 | border: none; 551 | border-radius: 500%; 552 | } 553 | 554 | 555 | /* ----------- */ 556 | #np-history-header button{ 557 | padding: 0.4rem; 558 | } 559 | 560 | #historyContainer{ 561 | max-height: 60vh; 562 | overflow-y: scroll; 563 | display: flex; 564 | flex-direction: column; 565 | } 566 | 567 | #historyContainer div{ 568 | /* width: 100%; */ 569 | width:15rem; 570 | /* max-height: 10vh; 571 | overflow-y: hidden; */ 572 | } 573 | 574 | .np-h-r-c{ 575 | /* width: 100%; */ 576 | max-height: 3rem; 577 | overflow: hidden; 578 | } 579 | 580 | 581 | /* ----------------------- */ 582 | 583 | .grid-align-line{ 584 | position: absolute; 585 | z-index: 900; 586 | border: 1px dashed lightgreen; 587 | } 588 | 589 | 590 | 591 | #freehandField { 592 | /* opacity: 0.1; */ 593 | /* background-color: #000; */ 594 | background-color: transparent; 595 | z-index: 1000; 596 | position: absolute; 597 | left: 0px; 598 | top:0px; 599 | 600 | cursor: crosshair; 601 | } 602 | 603 | #freehandField svg{ 604 | opacity: 1; 605 | position: absolute; 606 | left: 0px; 607 | top: 0px; 608 | width: 100%; 609 | height: 100%; 610 | 611 | /* z-index: -100; */ 612 | } 613 | 614 | /* 615 | #freehandField svg path{ 616 | fill:none; 617 | 618 | width:auto; 619 | } 620 | */ 621 | .svg{ 622 | pointer-events: none; 623 | } 624 | 625 | .svg .ui-rotatable-handle, .svg .ui-icon{ 626 | pointer-events: all; 627 | } 628 | 629 | .svg .np-n-tooltip{ 630 | pointer-events:all; 631 | } 632 | 633 | .svg path{ 634 | pointer-events: visiblePainted; 635 | } 636 | 637 | 638 | /* -------- */ 639 | #scale-box{ 640 | margin-left: -7vh; 641 | width: 5vh; 642 | bottom: 2vh; 643 | } 644 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Noteplace 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 104 |
105 | 106 | 107 | 166 |
167 | 168 |
169 |
170 | 171 |
172 | 181 | 182 |
183 | 184 |
185 | 186 | 187 | 188 |
189 |
190 | 191 |
192 |
193 |
194 |
195 |
196 | 197 | 198 | 199 |
200 | 201 | 202 | 203 | 228 | 229 | 248 |
249 | 257 |
258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 290 | 291 | -------------------------------------------------------------------------------- /history.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | properties of a history object: 4 | 5 | id : unique identifier, generates via newHistID 6 | type 7 | A : ADD 8 | D : DELETE 9 | M : MOVE, ? 10 | E : EDIT, ? 11 | 12 | state: {T:T,S:S} of a view where it happened 13 | 14 | timestamp: timestamp of that action 15 | 16 | node_ids: ids of node objects under said action in history 17 | 18 | oldValues : for MOVE and EDIT events old values of specified parameters 19 | ( are stored in _HISTORY ) 20 | newValues : for reverting aand going forward again 21 | 22 | nodes : for Actions, nodes to be created 23 | 24 | newValues : for Actions, new Values of MOVE/EDIT events 25 | (get converted to _HISTORY notation of oldValues via processAction) 26 | 27 | */ 28 | 29 | const btnUndo = _('#btnUndo'); 30 | const btnRedo = _('#btnRedo'); 31 | const btnHistoryStatus = _('#btnHistoryStatus'); 32 | const historyContainer = _('#historyContainer'); 33 | 34 | let _HISTORY = null; 35 | let _HISTORY_Map = new Map(); 36 | let _HISTORY_j_Map = new Map(); 37 | let _HISTORY_CURRENT_ID = null; // ID of the last applied history action 38 | 39 | function genHistIDMap () { 40 | _HISTORY_Map = new Map(_HISTORY.map(h => [h.id, h])); 41 | _HISTORY_Map.set(null, null); 42 | _HISTORY_j_Map = new Map([...Array(_HISTORY.length).keys()].map(j => [_HISTORY[j].id, j])); 43 | _HISTORY_j_Map.set(null, -1); 44 | } 45 | 46 | function getHistory (id) { 47 | return _HISTORY_Map.get(id); 48 | } 49 | 50 | function lastHistoryID () { 51 | return _HISTORY.length > 0 ? _HISTORY[_HISTORY.length - 1].id : null; 52 | } 53 | 54 | function newHistID (id = null) { 55 | return newID(id || 'h', getHistory); 56 | } 57 | 58 | function getAllHistoryProps(h){ 59 | const R = []; 60 | h.oldValues.forEach( vs => { 61 | Object.keys(vs).forEach( prop => { 62 | R.push(prop); 63 | }) 64 | }) 65 | return R; 66 | } 67 | 68 | function processAction (A) { 69 | // h = resulting history event 70 | const h = { type: A.type }; 71 | switch (h.type) { 72 | case 'A': 73 | // ADD 74 | let doms = []; 75 | if('node_ids' in A){ 76 | // just de-delete them, ok? 77 | h.node_ids = A.node_ids.slice(); 78 | 79 | h.node_ids.forEach( id => { 80 | idNode(id).deleted = false; 81 | }) 82 | }else{ 83 | h.node_ids = A.nodes.map(function (node) { 84 | doms.push(newNode(node, false)); 85 | return node.id; 86 | }); 87 | } 88 | // selectNode(doms); 89 | break; 90 | case 'D': 91 | // delete 92 | h.node_ids = A.node_ids.slice(); 93 | h.node_ids.forEach(n_id => { 94 | idNode(n_id).deleted = true; 95 | }); 96 | break; 97 | case 'M': 98 | // move and edit are basically the same 99 | case 'E': 100 | // edit 101 | h.node_ids = A.node_ids.slice(); 102 | h.oldValues = []; 103 | // h.newValues = []; 104 | let anythingChanged = false; 105 | const allProps = []; 106 | for (let j = 0; j < h.node_ids.length; j++) { 107 | const tnode = idNode(h.node_ids[j]); 108 | const oldValues = {}; 109 | const newValues = {}; 110 | Object.keys(A.newValues[j]).forEach(prop => { 111 | newValues[prop] = A.newValues[j][prop]; 112 | if(prop.indexOf('.')>0){ 113 | oldValues[prop] = ('oldValues' in A)?A.oldValues[j][prop]:dotProp(tnode,prop); 114 | setDotProp(tnode, prop, newValues[prop]); 115 | 116 | }else{ 117 | oldValues[prop] = ('oldValues' in A)?A.oldValues[j][prop]:tnode[prop]; 118 | tnode[prop] = newValues[prop]; 119 | } 120 | if(oldValues[prop] != newValues[prop]){ 121 | anythingChanged = true; 122 | } 123 | 124 | allProps.push(prop); 125 | }); 126 | h.oldValues.push(oldValues); 127 | // h.newValues.push(newValues); 128 | } 129 | if (h.type === 'E'){ 130 | // really redraw nodes 131 | h.node_ids.forEach( id => { newNode(idNode(id).dom, false); } ); 132 | } 133 | 134 | if(!anythingChanged){ 135 | return null; 136 | } 137 | 138 | if (_HISTORY.length > 0) { 139 | const lh = _HISTORY[_HISTORY.length - 1]; 140 | 141 | if (lh.type == h.type) { 142 | if (h.node_ids.length == lh.node_ids.length) { 143 | if(equalSetsOfItems(h.node_ids, lh.node_ids)) { 144 | if (equalSetsOfItems(allProps, getAllHistoryProps(lh))) { 145 | // same action basically, 146 | // +old Values stay the same, new ones - already applied. 147 | return null; 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | 155 | break; 156 | default: 157 | throw Error('processAction error: What type of history is [' + h.type + '] ??!'); 158 | break; 159 | } 160 | return h; 161 | } 162 | 163 | function applyAction (A) { 164 | /* 165 | * Applies Action A and saves it in _HISTORY 166 | * 167 | * 168 | */ 169 | log('applyAction'); 170 | log(JSON.stringify(A)); 171 | 172 | // if we are not at the end of _HISTORY, clear all after 173 | if (_HISTORY_CURRENT_ID !== lastHistoryID()) { 174 | _HISTORY = _HISTORY.slice(0, _HISTORY_j_Map.get(_HISTORY_CURRENT_ID) + 1); 175 | } 176 | 177 | // do the actual thing, make proper _HISTORY event 178 | const h = processAction(A); 179 | if(h !== null){ 180 | redraw(); 181 | 182 | h.id = newHistID(); 183 | h.timestamp = now(); 184 | // TODO: probably calculate state based on action itself? 185 | h.state = currentState(); 186 | 187 | // add to _HISTORY and to indices 188 | _HISTORY.push(h); 189 | _HISTORY_CURRENT_ID = h.id; 190 | _HISTORY_Map.set(h.id, h); 191 | _HISTORY_j_Map.set(h.id, _HISTORY.length - 1); 192 | 193 | // save? 194 | _localStorage.save(h.node_ids); 195 | 196 | fillHistoryList(); 197 | 198 | updateUndoRedoEnabled(); 199 | }else{ 200 | // nothing changed! 201 | log('no changes') 202 | } 203 | 204 | return h; 205 | } 206 | 207 | function revertHistory (id) { 208 | /* 209 | * 210 | * Reverts back specified history object 211 | * situation is supposed to be congruent with history 212 | * 213 | */ 214 | 215 | // get history object 216 | let h = id.hasOwnProperty('id') ? id:_HISTORY_Map.get(id); 217 | 218 | switch (h.type) { 219 | case 'A': 220 | // ADD 221 | // => delete 222 | h.node_ids.forEach(n_id => { 223 | idNode(n_id).deleted = true; 224 | }); 225 | break; 226 | case 'D': 227 | // delete 228 | // => un-delete ;) 229 | h.node_ids.forEach(n_id => { 230 | idNode(n_id).deleted = false; 231 | }); 232 | break; 233 | case 'M': 234 | // move 235 | // edit but with a fancy name hence no break 236 | case 'E': 237 | // edit 238 | log('reverting EDIT'); 239 | h.newValues = []; 240 | for (let j = 0; j < h.node_ids.length; j++) { 241 | const tnode = idNode(h.node_ids[j]); 242 | const newValues = {}; 243 | log(' node ' + idNode(h.node_ids[j]).id); 244 | for (let prop of Object.keys(h.oldValues[j])) { 245 | log(' prop ' + prop); 246 | if(prop.indexOf('.')>0){ 247 | // like style.color 248 | newValues[prop] = dotProp(tnode,prop); 249 | setDotProp(tnode, prop, h.oldValues[j][prop]); 250 | }else{ 251 | newValues[prop] = tnode[prop]; 252 | tnode[prop] = h.oldValues[j][prop]; 253 | } 254 | log(' -> ' + newValues[prop]); 255 | } 256 | h.newValues.push(newValues); 257 | } 258 | // sometimes an Update just won't cut it 259 | h.node_ids.forEach( id => { newNode(idNode(id).dom); } ); 260 | break; 261 | default: 262 | throw Error('revertHistory error: What type of history is [' + h.type + '] ??!'); 263 | break; 264 | } 265 | } 266 | 267 | function goBackInHistory () { 268 | log('goBackInHstory'); 269 | 270 | let nowj = _HISTORY_j_Map.get(_HISTORY_CURRENT_ID); 271 | log('current nowj=' + nowj); 272 | if (nowj === -1) { 273 | // before the first one : impossible! 274 | return 0; 275 | } 276 | 277 | revertHistory(_HISTORY[nowj]); 278 | 279 | gotoState(_HISTORY[nowj].state); 280 | 281 | nowj--; 282 | _HISTORY_CURRENT_ID = nowj >= 0 ? _HISTORY[nowj].id:null; 283 | log('nowj=' + nowj); 284 | log('_HISTORY_CURRENT_ID=' + _HISTORY_CURRENT_ID); 285 | } 286 | 287 | function goForwardInHistory () { 288 | log('goForwardInHistory'); 289 | 290 | let nowj = _HISTORY_j_Map.get(_HISTORY_CURRENT_ID); 291 | log('current nowj=' + nowj); 292 | if (nowj === _HISTORY.length - 1) { 293 | // after the last one : impossible! 294 | return 0; 295 | } 296 | 297 | nowj++; 298 | processAction(_HISTORY[nowj]); 299 | 300 | gotoState(_HISTORY[nowj].state); 301 | 302 | _HISTORY_CURRENT_ID = _HISTORY[nowj].id; 303 | log('nowj=' + nowj); 304 | log('_HISTORY_CURRENT_ID=' + _HISTORY_CURRENT_ID); 305 | } 306 | 307 | function clearAllHistory() { 308 | _HISTORY = []; 309 | _HISTORY_CURRENT_ID = null; 310 | // delete the deleted nodes ;) 311 | _NODES = _NODES.filter(n => ((!('deleted' in n)) || (n.deleted == false))) 312 | 313 | genHistIDMap(); 314 | fillHistoryList(); 315 | } 316 | 317 | // :::::::: ::::::::::: ::: ::::::::: ::::::::::: ::: ::: ::::::::: 318 | // :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 319 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 320 | // +#++:++#++ +#+ +#++:++#++: +#++:++#: +#+ +#+ +:+ +#++:++#+ 321 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 322 | // #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 323 | // ######## ### ### ### ### ### ### ######## ### 324 | 325 | if (localStorage['noteplace.history'] !== undefined) { 326 | try { 327 | _HISTORY = JSON.parse(localStorage['noteplace.history']); 328 | }catch (e) { 329 | _HISTORY = null; 330 | } 331 | } 332 | 333 | _HISTORY = _HISTORY || [ 334 | // { id: '0', type: 'A', state: { T: [0, 0], S: 1 }, timestamp: 1620040025793, node_ids: ['0', '1'] }, 335 | // { id: '1', type: 'D', state: { T: [0, 0], S: 1 }, timestamp: 1620040305793, node_ids: ['1'] }, 336 | // { id: '2', type: 'M', state: { T: [0, 0], S: 1 }, timestamp: 1620044005793, node_ids: ['0'], oldValues: [{ x: -100, y: -100 }] }, 337 | // { id: '3', type: 'E', state: { T: [-200, -200], S: 0.6 }, timestamp: 1620050025793, node_ids: ['0'], oldValues: [{ rotate: -0.3 }] } 338 | ]; 339 | 340 | genHistIDMap(); 341 | 342 | if (localStorage['noteplace.history_current'] !== undefined) { 343 | if (getHistory(localStorage['noteplace.history_current'])) { 344 | _HISTORY_CURRENT_ID = localStorage['noteplace.history_current']; 345 | }else { 346 | _HISTORY_CURRENT_ID = null; 347 | } 348 | } 349 | _HISTORY_CURRENT_ID = _HISTORY_CURRENT_ID || lastHistoryID(); 350 | 351 | function updateUndoRedoEnabled(){ 352 | btnUndo.disabled = (_HISTORY_CURRENT_ID === null); 353 | btnRedo.disabled = (_HISTORY_CURRENT_ID === lastHistoryID()); 354 | } 355 | 356 | btnUndo.onclick = function(e) { 357 | goBackInHistory(); 358 | updateUndoRedoEnabled(); 359 | fillHistoryList(); 360 | } 361 | 362 | btnRedo.onclick = function(e) { 363 | goForwardInHistory(); 364 | updateUndoRedoEnabled(); 365 | fillHistoryList(); 366 | } 367 | 368 | function fillHistoryList () { 369 | console.log("idNode"); 370 | console.log(idNode); 371 | historyContainer.innerHTML = ''; 372 | 373 | const nowDate = new Date(); 374 | for( let j=_HISTORY.length-1 ; j>=0 ; j-- ){ 375 | const h = _HISTORY[j]; 376 | 377 | const date = new Date(h.timestamp); 378 | 379 | const isToday = nowDate.toLocaleDateString() == date.toLocaleDateString(); 380 | // no need for date if it was today 381 | let datestr = isToday ? date.toLocaleTimeString() : date.toLocaleString(); 382 | datestr = datestr.replaceAll(' ',' '); 383 | 384 | let actionStr = ''; 385 | 386 | if (h.type === 'A') { 387 | actionStr = ''; 388 | }else if(h.type === 'D'){ 389 | actionStr = ''; 390 | }else if(h.type === 'M'){ 391 | actionStr = ''; 392 | }else if(h.type === 'E'){ 393 | actionStr = ''; 394 | } 395 | 396 | let nodeStr = ''; 397 | if(h.node_ids.length > 1) { 398 | nodeStr = h.node_ids.length + ' node' + (h.node_ids.length>1?'s':'') ; 399 | } else { 400 | var temp = idNode(h.node_ids[0]); 401 | if(temp) { 402 | nodeStr = temp.text.split('\n')[0]; 403 | }else{ 404 | nodeStr = '???'; 405 | console.error('No node with id=',h.node_ids[0]) 406 | } 407 | } 408 | 409 | let allstr = datestr + ' ' + actionStr + ' ' + nodeStr; 410 | 411 | const nodePreview = _ce('div' 412 | ,'className', 'np-h-r-c' 413 | ,'innerHTML', allstr 414 | ) 415 | 416 | const row = _ce('div' 417 | ,'className','row btn btn-outline-' + ( h.id == _HISTORY_CURRENT_ID ? 'primary' : 'secondary') 418 | ,'onmouseenter', function (e) { 419 | const _h_id = this.dataset['histodyID']; 420 | previewState(getHistory(_h_id).state); 421 | } 422 | ,'onmouseleave', function (e) { 423 | exitPreview(); 424 | } 425 | ,'onclick', function (e) { 426 | const _h_id = this.dataset['histodyID']; 427 | const clickJ = _HISTORY_j_Map.get(_h_id); 428 | const nowJ = _HISTORY_j_Map.get(_HISTORY_CURRENT_ID); 429 | 430 | let goBtn = btnUndo; 431 | if(nowJ < clickJ){ 432 | goBtn = btnRedo; 433 | } 434 | 435 | while(_HISTORY_CURRENT_ID !== _h_id){ 436 | goBtn.click(); 437 | } 438 | } 439 | ); 440 | row.dataset['histodyID'] = h.id; 441 | 442 | row.appendChild(nodePreview); 443 | // log(allstr); 444 | 445 | historyContainer.appendChild(row); 446 | } 447 | 448 | // start row 449 | 450 | const row = _ce('div' 451 | ,'className','row btn btn-outline-' + ( null == _HISTORY_CURRENT_ID ? 'primary' : 'secondary') 452 | ,'innerHTML','T = 0 , all STARTED' 453 | ,'onmouseenter', function (e) { 454 | _HISTORY.length > 0 ? previewState(_HISTORY[0].state) : ''; 455 | } 456 | ,'onmouseleave', function (e) { 457 | _HISTORY.length > 0 ? exitPreview() : ''; 458 | } 459 | ,'onclick', function (e) { 460 | while(_HISTORY_CURRENT_ID !== null){ 461 | btnUndo.click(); 462 | } 463 | } 464 | ); 465 | historyContainer.appendChild(row); 466 | 467 | // history status : how many changes, when it all started, .. 468 | 469 | let html = ''; 470 | let title = 'No history'; 471 | if(_HISTORY.length > 0) { 472 | let sameDay = false; 473 | if( (new Date(_HISTORY[0].timestamp)).toLocaleDateString 474 | == (new Date()).toLocaleDateString ) { 475 | // same day 476 | sameDay = true; 477 | html = ''; 478 | } else if ( ( now() - _HISTORY[0].timestamp ) < 24 * 3600 * 7 * 1000 ) { 479 | // same week 480 | html = ''; 481 | } else { 482 | html = ''; 483 | } 484 | const firstDate = (new Date(_HISTORY[0].timestamp)); 485 | const lastHistDate = new Date(_HISTORY[_HISTORY.length - 1].timestamp); 486 | 487 | title = _HISTORY.length + ' event' 488 | + ( (_HISTORY.length > 1) ? 's' : '') 489 | + ', ' + ( sameDay 490 | ? firstDate.toLocaleDateString() + ', from ' + firstDate.toLocaleTimeString() 491 | : 'from ' + firstDate.toLocaleString() 492 | ) 493 | + ' to ' + ( sameDay ? lastHistDate.toLocaleTimeString() : lastHistDate.toLocaleString() ) 494 | } 495 | 496 | btnHistoryStatus.innerHTML = html; 497 | btnHistoryStatus.title = title; 498 | } 499 | 500 | _('#btnHistoryClear').onclick = function () { 501 | showModalYesNo( 502 | 'Really?', 503 | 'Are you sure you want to delete all History?', 504 | function () { 505 | clearAllHistory(); 506 | } 507 | ) 508 | } 509 | -------------------------------------------------------------------------------- /places.js: -------------------------------------------------------------------------------- 1 | // _PLACES = {} 2 | 3 | _PLACES_dragContent = null; 4 | 5 | const _PLACES_default = { 6 | name: 'Places', 7 | items: [ 8 | { name: 'Home?', state: { T: [0, 0], S: 1 } } 9 | // {name:'Test folder', items:[ 10 | // {name:'test 1', state:{T:[-4000,-2000], S:0.1}}, 11 | // {name:'Test sub-folder..', items:[ 12 | // {name:'test 2', state:{T:[-400000,-200000], S:0.001}}, 13 | // {name:'test 3', state:{T:[-400000,-250000], S:0.001}}, 14 | // ]} 15 | // ]} 16 | ] 17 | }; 18 | 19 | function stripPlace (place = null) { 20 | if (place === null) { 21 | place = _PLACES; 22 | } 23 | 24 | if ('items' in place) { 25 | return { 26 | name: place.name, 27 | items: place.items.map(stripPlace) 28 | }; 29 | } else { 30 | return { 31 | name: place.name, 32 | state: { 33 | T: place.state.T.slice(), 34 | S: place.state.S 35 | } 36 | }; 37 | } 38 | } 39 | 40 | function placeOKNewName (path, new_name) { 41 | console.log('placeOKNewName path=' + path + ' new_name=' + new_name); 42 | if (!Array.isArray(path)) { 43 | path = JSON.parse(path); 44 | } 45 | 46 | return (pathPlace(path)// .slice(0,-1)) 47 | .items 48 | .map(s => s.name) 49 | .indexOf(new_name) === -1 50 | ); 51 | } 52 | 53 | // returns _PLACES for path 54 | function pathPlace (path) { 55 | let p = _PLACES; 56 | path = ((typeof (path) === 'string') ? JSON.parse(path) : path).forEach((e) => { 57 | p = p.items[p.items.map(jp => jp.name).indexOf(e)]; 58 | }); 59 | return p; 60 | } 61 | 62 | function placesUpdatePaths (places = null, _path = []) { 63 | if (!places) { 64 | places = _PLACES.items; 65 | } 66 | for (const place of places) { 67 | const path = _path.slice(); 68 | path.push(place.name); 69 | if ('items' in place) { 70 | place.dom.hBtn.dataset.path = JSON.stringify(path); 71 | 72 | placesUpdatePaths(place.items, path); 73 | } else { 74 | place.dom.a.dataset.path = JSON.stringify(path); 75 | } 76 | } 77 | } 78 | 79 | function placeNewName (path, folder = false) { 80 | let _name = 'New ' + (folder ? 'Folder':'Place'); 81 | let j = 0; 82 | while (!placeOKNewName(path, _name)) { 83 | j++; 84 | _name = 'New ' + (folder ? 'Folder':'Place') + ' ' + j; 85 | } 86 | return _name; 87 | } 88 | 89 | function addPlaceFolder (path) { 90 | console.log('addPlaceFolder path=' + path); 91 | const place = { name: placeNewName(path, true), items: [] }; 92 | const hpath = JSON.parse(path); 93 | hpath.push(place.name); 94 | 95 | const rli = createPlaceFolderDOM(place, JSON.stringify(hpath)); 96 | 97 | pathPlace(path).dom.ul.appendChild(rli); 98 | pathPlace(path).items.push(place); 99 | 100 | _localStorage.places.save(); 101 | 102 | place.dom.btnEdit.click(); 103 | } 104 | 105 | function addPlace (path) { 106 | console.log('addPlace path=' + path); 107 | 108 | const place = { name: placeNewName(path), state: _View.getState()}; 109 | const hpath = JSON.parse(path); 110 | hpath.push(place.name); 111 | 112 | const rli = createPlaceDOM(place, JSON.stringify(hpath)); 113 | 114 | pathPlace(path).dom.ul.appendChild(rli); 115 | pathPlace(path).items.push(place); 116 | 117 | _localStorage.places.save(); 118 | 119 | place.dom.btnEdit.click(); 120 | } 121 | 122 | function fillPlaces () { 123 | createPlaceFolderDOM.N = 0; 124 | _('#places-root').innerHTML = ''; 125 | _PLACES.dom = { 126 | hBtn: _('#btnPlaces'), 127 | ul: _('#places-root') 128 | }; 129 | rec_fillPlace(_PLACES.items, _('#places-root')); 130 | } 131 | 132 | function showPlacePath(path) { 133 | if (!Array.isArray(path)) { 134 | path = JSON.parse(path); 135 | } 136 | 137 | showMenu(); 138 | 139 | const btns2check = [_('#btnPlaces')]; 140 | for(var j=0 ; j<=path.length-1 ; j++){ 141 | const btn = pathPlace(path.slice(0, j)).dom.hBtn; 142 | if(btn.classList.contains('collapsed')){ 143 | btn.click(); 144 | } 145 | } 146 | } 147 | 148 | function previewPlacePath(path){ 149 | const place = pathPlace(path); 150 | const dom = 'items' in place ? place.dom.hBtn : place.dom.a; 151 | dom.classList.add('np-search-place-preview'); 152 | } 153 | 154 | function depreviewPlace(){ 155 | $('.np-search-place-preview').removeClass('np-search-place-preview'); 156 | } 157 | 158 | function rec_fillPlace (places, root_dom, _P_path = '') { 159 | console.log('rec_fillPlace path=' + _P_path); 160 | for (let place of places) { 161 | let path = _P_path ? JSON.parse(_P_path) : []; 162 | path.push(place.name); 163 | path = JSON.stringify(path); 164 | 165 | if ('items' in place) { 166 | // Folder 167 | const rli = createPlaceFolderDOM(place, path); 168 | 169 | console.log(' >rec_fillPlace with path=' + path); 170 | rec_fillPlace(place.items, place.dom.ul, path); 171 | 172 | root_dom.appendChild(rli); 173 | } else { 174 | // place, not a Folder 175 | root_dom.appendChild(createPlaceDOM(place, path)); 176 | } 177 | } 178 | } 179 | 180 | function createPlaceFolderDOM (place, path) { 181 | createPlaceFolderDOM.N++; 182 | 183 | const rli = document.createElement('li'); 184 | const hid = 'places-collapse-' + createPlaceFolderDOM.N; 185 | 186 | const hdiv = _ce('div'); 187 | 188 | const hBtn = _ce('span' 189 | , 'className', 'btn btn-toggle align-items-center collapsed places-folder places-name' 190 | , 'ariaExpanded', 'false' 191 | , 'innerHTML', place.name 192 | , 'title', 'Edit name' 193 | , 'onmouseenter', function (e) { 194 | console.log('mouseenter'); 195 | console.log(e); 196 | } 197 | , 'onmouseleave', function (e) { 198 | console.log('onmouseleave'); 199 | console.log(e); 200 | } 201 | ); 202 | hBtn.dataset.bsToggle = 'collapse'; 203 | hBtn.dataset.bsTarget = '#' + hid; 204 | hBtn.dataset.path = path; 205 | 206 | const btnEdit = _ce('button' 207 | , 'className', 'btn p-1' 208 | , 'innerHTML', '' 209 | , 'onclick', function (e) { 210 | const elt = this.parentNode.getElementsByClassName('places-name')[0]; 211 | elt.contentEditable = 'true'; 212 | elt.dataset.originalName = elt.innerHTML; 213 | elt.focus(); 214 | elt.addEventListener('focusout', function () { 215 | console.log('focusout1!'); 216 | console.log(this); 217 | console.log(this.innerHTML); 218 | this.contentEditable = 'false'; 219 | if (this.innerHTML != this.dataset.originalName) { 220 | const thisplace = pathPlace(this.dataset.path); 221 | const tpath = JSON.parse(this.dataset.path).slice(0, -1); 222 | if (placeOKNewName(tpath, this.innerHTML)) { 223 | tpath.push(this.innerHTML); 224 | this.dataset.path = JSON.stringify(tpath); 225 | thisplace.name = this.innerHTML; 226 | placesUpdatePaths(thisplace.items, tpath); 227 | save('places'); 228 | } else { 229 | // return to original 230 | this.innerHTML = this.dataset.originalName; 231 | } 232 | } 233 | }); 234 | elt.addEventListener('keydown', function (e) { 235 | console.log(e); 236 | if (e.key === 'Enter') { 237 | // end editing 238 | this.contentEditable = 'false'; 239 | } else if (e.key === ' ') { 240 | // e.preventDefault(); 241 | // this.innerHTML+=' '; 242 | // e.stopPropagation(); 243 | } 244 | }); 245 | } 246 | ); 247 | 248 | const btnDel = _ce('button' 249 | , 'className', 'btn text-danger p-1' 250 | , 'innerHTML', '' 251 | , 'title', 'Delete folder' 252 | , 'onclick', function (e) { 253 | showModalYesNo( 254 | 'Really?' 255 | , 'Are you sure you want to delete folder ' + place.name + ' with all it\'s contents???' 256 | , function () { 257 | // find parent place 258 | const path = place.dom.hBtn.dataset.path; 259 | let parent_place = pathPlace(JSON.parse(path).slice(0, -1)); 260 | 261 | if ('items' in parent_place) { 262 | parent_place = parent_place.items; 263 | } 264 | // remove place 265 | parent_place.splice(parent_place.indexOf(place), 1); 266 | 267 | // remove dom 268 | place.dom.hBtn.parentNode.parentNode.parentNode.removeChild(place.dom.hBtn.parentNode.parentNode); 269 | 270 | save('places'); 271 | } 272 | ); 273 | } 274 | ); 275 | 276 | const btnAddFolder = _ce('button' 277 | , 'className', 'btn p-1' 278 | , 'innerHTML', '' 279 | , 'title', 'Add sub-folder' 280 | , 'onclick', function (e) { 281 | if (hBtn.ariaExpanded == 'false') { 282 | hBtn.click(); 283 | } 284 | addPlaceFolder(place.dom.hBtn.dataset.path); 285 | } 286 | ); 287 | 288 | const btnAddPlace = _ce('button' 289 | , 'className', 'btn p-1' 290 | , 'innerHTML', '' 291 | , 'title', 'Add place to folder' 292 | , 'onclick', function (e) { 293 | if (hBtn.ariaExpanded == 'false') { 294 | hBtn.click(); 295 | } 296 | addPlace(place.dom.hBtn.dataset.path); 297 | } 298 | ); 299 | 300 | const bdiv = _ce('div' 301 | , 'className', 'collapse' 302 | , 'id', hid 303 | ); 304 | 305 | const tul = _ce('ul' 306 | , 'className', 'btn-toggle-nav list-unstyled fw-normal pb-1 small' 307 | ); 308 | bdiv.appendChild(tul); 309 | 310 | hdiv.appendChild(hBtn); 311 | hdiv.appendChild(btnEdit); 312 | hdiv.appendChild(btnDel); 313 | hdiv.appendChild(btnAddFolder); 314 | hdiv.appendChild(btnAddPlace); 315 | 316 | place.dom = { 317 | hBtn: hBtn, 318 | btnEdit: btnEdit, 319 | btnDel: btnDel, 320 | btnAddFolder: btnAddFolder, 321 | btnAddPlace: btnAddPlace, 322 | ul: tul 323 | }; 324 | rli.appendChild(hdiv); 325 | rli.appendChild(bdiv); 326 | 327 | return rli; 328 | } 329 | 330 | function createPlaceDOM (place, path) { 331 | const rli = _ce('li' 332 | ); 333 | 334 | const ta = _ce('a' 335 | , 'className', 'btn col align-self-start align-items-center places-place places-name' 336 | , 'innerHTML', place.name 337 | , 'onclick', function (e) { 338 | gotoState(pathPlace(this.dataset.path).state, false, true); 339 | } 340 | , 'onmouseenter', function (e) { 341 | console.log('enter'); 342 | previewState(pathPlace(this.dataset.path).state); 343 | } 344 | , 'onmouseleave', function (e) { 345 | console.log('leave'); 346 | exitPreview(); 347 | } 348 | ); 349 | ta.dataset.path = path; 350 | 351 | const btnEdit = _ce('button' 352 | , 'className', 'btn p-1' 353 | , 'innerHTML', '' 354 | , 'title', 'Edit name' 355 | , 'onclick', function (e) { 356 | const elt = this.parentNode.getElementsByClassName('places-name')[0]; 357 | elt.contentEditable = 'true'; 358 | // save for probable collision check 359 | elt.dataset.originalName = elt.innerHTML; 360 | elt.focus(); 361 | elt.addEventListener('focusout', function () { 362 | console.log('focusout1!'); 363 | console.log(this); 364 | this.contentEditable = 'false'; 365 | if (this.innerHTML != this.dataset.originalName) { 366 | console.log(this.dataset.path); 367 | const thisplace = pathPlace(this.dataset.path); 368 | const tpath = JSON.parse(this.dataset.path).slice(0, -1); 369 | if (placeOKNewName(tpath, this.innerHTML)) { 370 | console.log('OK NAME!'); 371 | console.log(this.dataset.path); 372 | tpath.push(this.innerHTML); 373 | this.dataset.path = JSON.stringify(tpath); 374 | thisplace.name = this.innerHTML; 375 | console.log(this.dataset.path); 376 | save('places'); 377 | } else { 378 | this.innerHTML = this.dataset.originalName; 379 | } 380 | } 381 | }); 382 | elt.addEventListener('keydown', function (e) { 383 | if (e.key == 'Enter') { 384 | // end editing 385 | this.contentEditable = 'false'; 386 | e.preventDefault(); 387 | e.stopPropagation(); 388 | } 389 | }); 390 | } 391 | ); 392 | 393 | const btnDel = _ce('button' 394 | , 'className', 'btn text-danger p-1' 395 | , 'innerHTML', '' 396 | , 'title', 'Delete place' 397 | , 'onclick', function (e) { 398 | showModalYesNo( 399 | 'Really?' 400 | , 'Are you sure you want to delete ' + place.name + '?' 401 | , function () { 402 | // find parent place 403 | let path = place.dom.a.dataset.path; 404 | parent_place = pathPlace(JSON.parse(path).slice(0, -1)); 405 | 406 | if ('items' in parent_place) { 407 | parent_place = parent_place.items; 408 | } 409 | // remove place 410 | parent_place.splice(parent_place.indexOf(place), 1); 411 | 412 | // remove dom 413 | place.dom.a.parentNode.parentNode.removeChild(place.dom.a.parentNode); 414 | 415 | save('places'); 416 | } 417 | ); 418 | } 419 | ); 420 | 421 | const btnSubs = _ce('button' 422 | , 'className', 'btn p-1' 423 | , 'innerHTML', '' 424 | , 'title', 'Save current here' 425 | , 'onclick', function (e) { 426 | showModalYesNo( 427 | 'Rewrite?' 428 | , 'Are you sure you want to save current position as ' + place.name + '?' 429 | , function () { 430 | place.state = { T: T, S: S }; 431 | save('places'); 432 | } 433 | ); 434 | } 435 | ); 436 | 437 | const btnPlace = _ce('button' 438 | , 'className', 'btn p-1 col align-self-end' 439 | , 'innerHTML', '' 440 | , 'title', 'Drag to place link' 441 | , 'draggable', true 442 | , 'ondragstart', function (e) { 443 | console.log('place drag start'); 444 | console.log(e); 445 | e.dataTransfer.effectAllowed = 'all'; 446 | _PLACES_dragContent = 'Tap to [' + place.name + '](' + getStateURL(place.state) + ' "Goto ' + place.name + '")'; 447 | e.dataTransfer.setData('text', _PLACES_dragContent); 448 | console.log(e); 449 | } 450 | , 'ondragend', function (e) { 451 | _PLACES_dragContent = null; 452 | } 453 | , 'ondrop', function (e) { 454 | _PLACES_dragContent = null; 455 | } 456 | ); 457 | 458 | rli.appendChild(ta); 459 | rli.appendChild(btnEdit); 460 | rli.appendChild(btnDel); 461 | rli.appendChild(btnSubs); 462 | rli.appendChild(btnPlace); 463 | 464 | place.dom = { 465 | a: ta, 466 | btnEdit: btnEdit, 467 | btnDel: btnDel, 468 | btnSubs: btnSubs 469 | }; 470 | 471 | return rli; 472 | } 473 | 474 | // :::::::: ::::::::::: ::: ::::::::: ::::::::::: ::: ::: ::::::::: 475 | // :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 476 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 477 | // +#++:++#++ +#+ +#++:++#++: +#++:++#: +#+ +#+ +:+ +#++:++#+ 478 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 479 | // #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 480 | // ######## ### ### ### ### ### ### ######## ### 481 | 482 | // :::::::::: ::: ::: :::::::::: :::: ::: ::::::::::: :::::::: 483 | // :+: :+: :+: :+: :+:+: :+: :+: :+: :+: 484 | // +:+ +:+ +:+ +:+ :+:+:+ +:+ +:+ +:+ 485 | // +#++:++# +#+ +:+ +#++:++# +#+ +:+ +#+ +#+ +#++:++#++ 486 | // +#+ +#+ +#+ +#+ +#+ +#+#+# +#+ +#+ 487 | // #+# #+#+#+# #+# #+# #+#+# #+# #+# #+# 488 | // ########## ### ########## ### #### ### ######## 489 | 490 | _('#btnNewHomeSubFolder').addEventListener('click', (e) => { 491 | const domwpath = _('#btnPlaces'); 492 | if (domwpath.ariaExpanded == 'false') { 493 | domwpath.click(); 494 | } 495 | 496 | addPlaceFolder('[]'); 497 | }); 498 | 499 | _('#btnNewHomeSubPlace').addEventListener('click', (e) => { 500 | const domwpath = _('#btnPlaces'); 501 | if (domwpath.ariaExpanded == 'false') { 502 | domwpath.click(); 503 | } 504 | addPlace('[]'); 505 | }); 506 | 507 | 508 | 509 | _('#btnSaveView').dataset.view = null; 510 | _('#btnSaveView').onclick = function () { 511 | const btn = _('#btnSaveView'); 512 | btn.dataset.view = getStateURL(); 513 | 514 | btn.innerHTML = ' '; 515 | // btn.getElementsByTagName('i')[0].className='bi-stickies-fill'; 516 | // btn.classList.remove('btn-secondary'); 517 | // btn.classList.add('btn-success'); 518 | 519 | btn.title = 'Saved View ' + btoa(Math.random()).slice(10, 13) + ' '; 520 | 521 | const telt = _ce('i' 522 | , 'className', 'bi-fullscreen' 523 | , 'title', 'Preview' 524 | ); 525 | telt.addEventListener('mouseenter', e => { 526 | previewState(_('#btnSaveView').dataset.view); 527 | }); 528 | telt.addEventListener('mouseleave', e => { 529 | exitPreview(); 530 | }); 531 | telt.addEventListener('click', (e) => { 532 | e.stopPropagation(); 533 | __previewOldState = { T: T, S: S }; 534 | // gotoState(_('#btnSaveView').dataset['view'], false, true); 535 | }, false); 536 | 537 | btn.appendChild(telt); 538 | 539 | btn.title = btn.dataset.view; 540 | btn.draggable = true; 541 | 542 | btn.ondragstart = function (e) { 543 | console.log('btn drag start'); 544 | console.log(e); 545 | e.dataTransfer.effectAllowed = 'all'; 546 | _PLACES_dragContent = 'Tap to [View](' + this.dataset.view + ' "Saved View")'; 547 | e.dataTransfer.setData('text', _PLACES_dragContent); 548 | console.log(e); 549 | }; 550 | 551 | btn.ondragend = function (e) { 552 | console.log('btn ondragend'); 553 | _PLACES_dragContent = null; 554 | console.log(e); 555 | }; 556 | 557 | btn.ondrop = function (e) { 558 | console.log('btn drop'); 559 | _PLACES_dragContent = null; 560 | console.log(e); 561 | }; 562 | }; 563 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | let M = 0; 2 | 3 | let _selected_DOM = []; 4 | 5 | let width = (window.innerWidth || document.documentElement.clientWidth || BODY.clientWidth); 6 | let height = window.innerHeight || document.documentElement.clientHeight || BODY.clientHeight; 7 | 8 | let resizeWatchTimeout = null; 9 | window.addEventListener('resize', function (event) { 10 | // do stuff here 11 | clearTimeout(resizeWatchTimeout); 12 | resizeWatchTimeout = setTimeout(function () { 13 | width = (window.innerWidth || document.documentElement.clientWidth || BODY.clientWidth); 14 | height = window.innerHeight || document.documentElement.clientHeight || BODY.clientHeight; 15 | 16 | }, 500); // run update only every 100ms 17 | }); 18 | 19 | const _FreeHand = new FreeHand() 20 | 21 | node_container.ondblclick = function (e) { 22 | console.log('dblclick on empty field at [' + e.clientX + ',' + e.clientY + ']'); 23 | console.log(_View.state); 24 | applyAction({ 25 | type: 'A', 26 | nodes: [ 27 | { text: 'test' + _NODES.length } 28 | ] 29 | }); 30 | 31 | e.preventDefault(); 32 | e.stopPropagation(); 33 | 34 | setTimeout(function () { 35 | onNodeDblClick(_NODES[_NODES.length - 1].dom); 36 | }, 10); 37 | }; 38 | 39 | function haveNodesSelection(){ 40 | return _selected_DOM.length > 0; 41 | } 42 | 43 | function zoomInOut (inDegree, clientPos = null) { 44 | let centerX = 0; 45 | let centerY = 0; 46 | if (clientPos == null) { 47 | // basically - we pressed a +/- button 48 | if (haveNodesSelection()) { 49 | centerPos = calcCenterPos(_selected_DOM.map(domNode)) 50 | 51 | clientPos = _View.posToClient(centerPos) 52 | } else { 53 | // just center 54 | clientPos = [width / 2, height / 2]; 55 | } 56 | } 57 | _View.changeZoom( 58 | Math.pow(zoomK, inDegree), 59 | clientPos 60 | ) 61 | } 62 | 63 | function fitInBorders(x, minX, maxX){ 64 | return ( x > maxX ) ? maxX : ( x < minX ? minX : x); 65 | } 66 | 67 | const wheelZoom_minInterval_ms = 10; 68 | 69 | function onMouseWheel (e) { 70 | // console.log(e); 71 | e.preventDefault(); 72 | e.stopPropagation(); 73 | // console.log(e.deltaX, e.deltaY, e.deltaFactor); 74 | let hdelta = e.deltaY < 0 ? 1 : -1; 75 | if ((e.ctrlKey) && (_selected_DOM.length > 0)) { 76 | editFontSize(hdelta); 77 | }else { 78 | if (now() - onMouseWheel.lastZoom > wheelZoom_minInterval_ms) { 79 | if (e.ctrlKey) { 80 | hdelta = -e.deltaY / 10; 81 | } 82 | zoomInOut(hdelta, [e.clientX, e.clientY]); 83 | onMouseWheel.lastZoom = now(); 84 | } 85 | } 86 | } 87 | onMouseWheel.lastZoom = null; 88 | 89 | // $(container).on('mousewheel', onMouseWheel ); 90 | // _('#container').addEventListener("wheel", onMouseWheel); 91 | // _('#node_container').addEventListener("wheel", onMouseWheel); 92 | 93 | // $('#container').bind('mousewheel DOMMouseScroll', onMouseWheel); 94 | // $('#container').scroll(onMouseWheel) 95 | 96 | // safari? 97 | // window.onwheel = onMouseWheel; 98 | // window.addEventListener("wheel",onMouseWheel); 99 | _('#wrapper').onwheel = onMouseWheel; 100 | 101 | // Safari zoomes on pinch no matter what 102 | // all of these do not work =( 103 | window.addEventListener('gestureend', e => { 104 | console.log('gestureend'); 105 | console.log(e); 106 | e.preventDefault(); 107 | }); 108 | window.addEventListener('gesturestart', e => { 109 | console.log('gesturestart'); 110 | console.log(e); 111 | e.preventDefault(); 112 | }); 113 | window.addEventListener('gesturechange', e => { 114 | console.log('gesturechange'); 115 | console.log(e); 116 | e.preventDefault(); 117 | }); 118 | 119 | function setTransitionDur (s) { 120 | $('.node').css('transition-duration', s + 's'); 121 | // $('.np-n-c').css('transition-duration', s + 's'); 122 | $('#container img').css('transition-duration', s + 's'); 123 | $('#container svg').css('transition-duration', s + 's'); 124 | // $('#container path').css('transition-duration', s + 's'); 125 | // $('#container .ui-wrapper').css('transition-duration', s + 's'); 126 | } 127 | 128 | let _Mouse = { 129 | is: { 130 | down: false, 131 | downContentEdit: false, 132 | downPath: false, 133 | dragging: false, 134 | dragSelecting: false, 135 | resizing: false, 136 | }, 137 | pos: [0, 0], 138 | clipboard: [], // stores nodes with relative coordinates (x-centerX)*S 139 | dragSelected: [], 140 | rotatingNode: null, 141 | down:{ 142 | pos: [0,0], 143 | T: [0,0], 144 | node: null, 145 | }, 146 | drag:{ 147 | start: [0, 0], 148 | pos: [0, 0], 149 | node: null, 150 | }, 151 | } 152 | 153 | node_container.onmousedown = function (e) { 154 | console.log('container.onmousedown'); 155 | console.log('T=' + _View.state.T + ' S=' + _View.state.S); 156 | 157 | if (_Mouse.is.resizing) { 158 | console.log('resizing..'); 159 | } else { 160 | if (_Mouse.is.downContentEdit) { 161 | log('_Mouse.is.downContentEdit') 162 | _Mouse.is.downContentEdit = false; 163 | } else { 164 | log('!_Mouse.is.downContentEdit') 165 | _Mouse.down.pos = [e.clientX, e.clientY]; 166 | _Mouse.down.T = [_View.state.T[0],_View.state.T[1]]; 167 | _Mouse.is.down = true; 168 | 169 | if (_ContentEditing.textarea) { 170 | log('_ContentEditing.textarea') 171 | _ContentEditing.stop(); 172 | } 173 | } 174 | 175 | if (e.shiftKey) { 176 | _Mouse.is.dragSelecting = true; 177 | 178 | _('#select-box').style.display = 'block'; 179 | _('#select-box').style.width = 0; 180 | _('#select-box').style.height = 0; 181 | _('#select-box').style.top = e.clientX.toPx(); 182 | _('#select-box').style.left = e.clientY.toPx(); 183 | } else { 184 | if (_Mouse.down.node) { 185 | // pass 186 | log('_Mouse.down.node'); 187 | } else { 188 | log('!_Mouse.down.node'); 189 | // throw Error('aaa'); 190 | setTimeout(function(){selectNode(null);},50); 191 | } 192 | } 193 | } 194 | }; 195 | 196 | function hideSelectBox(){ 197 | _('#select-box').style.display = 'none'; 198 | _('#select-box').style.width = 0; 199 | _('#select-box').style.height = 0; 200 | _('#select-box').style.top = 0; 201 | _('#select-box').style.left = 0; 202 | } 203 | 204 | window.addEventListener('mouseup', function (e) { 205 | console.log('window onmouseup'); 206 | // console.log('T=' + T + ' S=' + S); 207 | 208 | _Mouse.down.node = null; 209 | 210 | if (_Mouse.drag.node) { 211 | // stop dragging 212 | const A = { type: 'M' } 213 | 214 | if (_selected_DOM.contains(_Mouse.drag.node.dom)) { 215 | // moving all selected 216 | A.node_ids = _selected_DOM.map( dom => domNode(dom).id ); 217 | } else { 218 | // moving just the one node 219 | A.node_ids = [ _Mouse.drag.node.id ]; 220 | } 221 | A.oldValues = A.node_ids.map( id => idNode(id).startPos ); 222 | A.newValues = A.node_ids.map( id => { 223 | const node = idNode(id); 224 | return { x: node.x, y: node.y} 225 | }); 226 | 227 | applyAction(A); 228 | // save(_Mouse.drag.node); 229 | 230 | _Mouse.drag.node = false; 231 | } else if (_Mouse.is.dragSelecting) { 232 | hideSelectBox(); 233 | 234 | _Mouse.is.dragSelecting = false; 235 | 236 | console.log('updating _Mouse.dragSelected..'); 237 | updateDragSelect(); 238 | console.log('now ' + _Mouse.dragSelected.length + ' _Mouse.dragSelected'); 239 | 240 | console.log('pushing _Mouse.dragSelected doms to _selected_DOM'); 241 | _Mouse.dragSelected.forEach((node) => { 242 | console.log(node.dom.id); 243 | _selected_DOM.push(node.dom); 244 | }); 245 | _Mouse.dragSelected = []; 246 | } else if (_Mouse.rotatingNode) { 247 | 248 | rotateStop({}, {angle: { current: 0 } } ); 249 | 250 | } else if (_Mouse.is.down) { 251 | // stop moving 252 | _View.state.T[0] = _Mouse.down.T[0] - 1 * node_container.dataset.x / _View.state.S; 253 | _View.state.T[1] = _Mouse.down.T[1] - 1 * node_container.dataset.y / _View.state.S; 254 | 255 | node_container.dataset.x = 0; 256 | node_container.dataset.y = 0; 257 | 258 | node_container.style.left = 0; 259 | node_container.style.top = 0; 260 | 261 | replaceHistoryState(); 262 | 263 | redraw(); 264 | } 265 | 266 | hideGridLine(); 267 | _Mouse.is.resizing = false; 268 | _Mouse.is.down = false; 269 | _Mouse.is.downPath = false; 270 | }); 271 | 272 | function tempSelect (node) { 273 | if (_selected_DOM.indexOf(node.dom) >= 0) { 274 | // already really selected 275 | } else { 276 | if (_Mouse.dragSelected.indexOf(node) < 0) { 277 | node.dom.classList.add('selected'); 278 | 279 | _Mouse.dragSelected.push(node); 280 | } 281 | } 282 | } 283 | 284 | function tempDeselect (node) { 285 | console.log('deselecting: ' + node.dom.id); 286 | if (_selected_DOM.indexOf(node.dom) >= 0) { 287 | // selected previously.. 288 | } else { 289 | node.dom.classList.remove('selected'); 290 | _Mouse.dragSelected.splice(_Mouse.dragSelected.indexOf(node), 1); 291 | } 292 | } 293 | 294 | function updateDragSelect () { 295 | // so the function os not run two times simultaneously 296 | if (!('on' in updateDragSelect)) { 297 | updateDragSelect.on = true; 298 | } else { 299 | if (updateDragSelect.on) { 300 | setTimeout(updateDragSelect, 100); 301 | return 0; 302 | } 303 | } 304 | // 305 | _Mouse.dragSelected.forEach((node) => { 306 | node.stillSelected = false; 307 | }); 308 | _NODES.forEach((node) => { 309 | // if(node.vis){ 310 | if (!node.deleted) { 311 | if (nodeIsInClientBox(node, 312 | [_Mouse.pos, _Mouse.down.pos] 313 | )) { 314 | tempSelect(node); 315 | node.stillSelected = true; 316 | } 317 | } 318 | }); 319 | // 320 | _Mouse.dragSelected.forEach((node) => { 321 | if (node.stillSelected === false) { 322 | tempDeselect(node); 323 | } 324 | }); 325 | // 326 | updateDragSelect.on = false; 327 | } 328 | 329 | function isNodeInBox (node, bxMin, bxMax, byMin, byMax) { 330 | return isInBox( 331 | node.x, node.xMax, node.y, node.yMax, 332 | bxMin, bxMax, byMin, byMax 333 | ); 334 | } 335 | 336 | function nodeBox(node){ 337 | return [[node.x, node.y], [node.xMax, node.yMax]]; 338 | } 339 | 340 | function nodeIsInBox(node, box){ 341 | return isInBox( nodeBox(node), box ); 342 | } 343 | 344 | function nodeIsInClientBox(node, cBox){ 345 | return nodeIsInBox(node, cBox.map((pos)=>clientToPos(pos))); 346 | } 347 | 348 | function allVisibleNodesProps(prop='x', except=[]){ 349 | const except_ids = except.map( n => n.id ) 350 | const R = new Map(); 351 | for( let j=0 ; j<_NODES.length ; j++){ 352 | const node = _NODES[j]; 353 | if ((!node.vis)||(node.deleted)) 354 | continue; 355 | if (except_ids.indexOf(node.id) >= 0) 356 | continue; 357 | 358 | R.set(node.id, node[prop]); 359 | } 360 | return R; 361 | } 362 | 363 | _theGridAlignLines = { x: null, y:null }; 364 | 365 | function gridAlignLine(node1, node2, prop){ 366 | if(_theGridAlignLines[prop] == null){ 367 | _theGridAlignLines[prop] = _ce('div' 368 | ,'className','grid-align-line' 369 | ); 370 | node_container.appendChild(_theGridAlignLines[prop]); 371 | } 372 | _theGridAlignLines[prop].style.display = ''; 373 | const prop2 = 'y'; 374 | 375 | let widthprop = 'width'; 376 | let heightprop = 'height'; 377 | let domProp = 'left'; 378 | let domProp2 = 'top'; 379 | if(prop == 'y'){ 380 | widthprop = 'height'; 381 | heightprop = 'width'; 382 | domProp = 'top'; 383 | domProp2 = 'left'; 384 | } 385 | 386 | const delta = 0; 387 | 388 | node1prop = node1.dom.style[domProp].pxToFloat(); 389 | node2prop = node2.dom.style[domProp].pxToFloat(); 390 | node1prop2 = node1.dom.style[domProp2].pxToFloat(); 391 | node2prop2 = node2.dom.style[domProp2].pxToFloat(); 392 | 393 | _theGridAlignLines[prop].style[widthprop] = 1; 394 | _theGridAlignLines[prop].style[domProp] = Math.min( node1prop , node2prop ).toPx(); 395 | 396 | let tx = Math.min( node1prop2 , node2prop2) - delta; 397 | // log('tx='); 398 | // log(tx); 399 | 400 | _theGridAlignLines[prop].style[domProp2] = tx.toPx(); 401 | // log(_theGridAlignLines[prop].style[domProp2]) 402 | _theGridAlignLines[prop].style[heightprop] = (Math.max( node1prop2 , node2prop2) - tx + 2 * delta ).toPx(); 403 | 404 | } 405 | 406 | function hideGridLine(prop){ 407 | // log(prop); 408 | if(prop == null){ 409 | Object.keys(_theGridAlignLines).forEach( hideGridLine ); 410 | return 0; 411 | } 412 | if(_theGridAlignLines[prop] !== null) { 413 | _theGridAlignLines[prop].style.display = 'none'; 414 | } 415 | } 416 | 417 | container.onmousemove = function (e) { 418 | _Mouse.pos = [e.clientX, e.clientY]; 419 | if (_Mouse.is.down) { 420 | if (_Mouse.drag.node) { 421 | const deltaMove = { 422 | x: (e.clientX - _Mouse.drag.start[0]) / _View.state.S , 423 | y: (e.clientY - _Mouse.drag.start[1]) / _View.state.S 424 | } 425 | const sizeScreen = { 426 | x: width, 427 | y: height 428 | } 429 | let updatePosNodes = [_Mouse.drag.node]; 430 | 431 | if (_Mouse.drag.node.dom.classList.contains('selected')) { 432 | // move all selected 433 | updatePosNodes = _selected_DOM.map(domNode); 434 | } 435 | 436 | for(let prop of ['x','y']){ 437 | _Mouse.drag.node[prop] = _Mouse.drag.node.startPos[prop] + deltaMove[prop]; 438 | 439 | allPropsMap = allVisibleNodesProps(prop, [_Mouse.drag.node]); 440 | 441 | allPropsAbs = [...allPropsMap.values()].map( v => Math.abs( v - _Mouse.drag.node[prop] )); 442 | minAbs = Math.min(...allPropsAbs) 443 | minJ = allPropsAbs.indexOf(minAbs); 444 | 445 | minDvh = minAbs * _View.state.S / sizeScreen[prop]; 446 | 447 | if(minDvh < 0.01){ 448 | // 449 | // log('seems like node '+[...allPropsMap.keys()][minJ]+' is OK, huh?'); 450 | // _Mouse.drag.node[prop] = [...allPropsMap.values()][minJ]; 451 | deltaMove[prop] = [...allPropsMap.values()][minJ] - _Mouse.drag.node.startPos[prop]; 452 | 453 | gridAlignLine(_Mouse.drag.node, idNode([...allPropsMap.keys()][minJ]), prop); 454 | } else { 455 | hideGridLine(prop); 456 | } 457 | } 458 | updatePosNodes.forEach(function (node) { 459 | node.x = node.startPos.x + deltaMove.x; 460 | node.y = node.startPos.y + deltaMove.y; 461 | calcBox(node); 462 | updateNode(node); 463 | }); 464 | // move the node under the cursor 465 | // _Mouse.drag.node.x = _Mouse.drag.node.startPos.x + deltaMove.x; 466 | // _Mouse.drag.node.y = _Mouse.drag.node.startPos.y + deltaMove.y; 467 | // calcBox(_Mouse.drag.node); 468 | // updateNode(_Mouse.drag.node); 469 | // } 470 | } else if (_Mouse.is.resizing) { 471 | // pass 472 | } else if (_Mouse.is.dragSelecting) { 473 | _('#select-box').style.left = Math.min(e.clientX, _Mouse.down.pos[0]).toPx(); 474 | _('#select-box').style.width = Math.abs(e.clientX - _Mouse.down.pos[0]).toPx(); 475 | _('#select-box').style.top = Math.min(e.clientY, _Mouse.down.pos[1]).toPx(); 476 | _('#select-box').style.height = Math.abs(e.clientY - _Mouse.down.pos[1]).toPx(); 477 | 478 | updateDragSelect(); 479 | // clearTimeout(_dragSelectingTimeout); 480 | // _dragSelectingTimeout = setTimeout(updateDragSelect, 500); 481 | } else { 482 | // T[0] = _Mouse.down.T[0] - (e.clientX - _Mouse.down.pos[0])/S; 483 | // T[1] = _Mouse.down.T[1] - (e.clientY - _Mouse.down.pos[1])/S; 484 | node_container.dataset.x = (e.clientX - _Mouse.down.pos[0]); 485 | node_container.dataset.y = (e.clientY - _Mouse.down.pos[1]); 486 | node_container.style.left = node_container.dataset.x.toPx(); 487 | node_container.style.top = node_container.dataset.y.toPx(); 488 | 489 | // redraw() 490 | } 491 | } 492 | 493 | if(_previewNode){ 494 | _previewNode.style.left = e.clientX; 495 | _previewNode.style.top = e.clientY; 496 | } 497 | }; 498 | 499 | _previewNode = null; 500 | 501 | // ::: ::: :::::::::: ::: ::: ::::::::: :::::::: ::: ::: :::: ::: 502 | // :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:+: :+: 503 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ 504 | // +#++:++ +#++:++# +#++: +#+ +:+ +#+ +:+ +#+ +:+ +#+ +#+ +:+ +#+ 505 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+#+# 506 | // #+# #+# #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+#+# 507 | // ### ### ########## ### ######### ######## ### ### ### #### 508 | 509 | 510 | function calcCenterPos (nodes) { 511 | const R = [0, 0]; 512 | nodes.forEach(function (node) { 513 | R[0] += node.x; 514 | R[1] += node.y; 515 | }); 516 | return [R[0] / nodes.length, R[1] / nodes.length]; 517 | } 518 | 519 | function copySelectedNodesToClipboard (de_id = false) { 520 | if (('on' in copySelectedNodesToClipboard) && (copySelectedNodesToClipboard.on)) { 521 | // pass 522 | } else { 523 | copySelectedNodesToClipboard.on = true; 524 | _Mouse.clipboard = _selected_DOM.map(domNode).map(stripNode); 525 | const centerPos = calcCenterPos(_Mouse.clipboard); 526 | _Mouse.clipboard.forEach((node) => { 527 | // yeah, my definition of S is counterintuitive here.. 528 | node.x = (node.x - centerPos[0]) * _View.state.S; 529 | node.y = (node.y - centerPos[1]) * _View.state.S; 530 | node.fontSize *= _View.state.S; 531 | }); 532 | 533 | copyToClipboard(JSON.stringify(_Mouse.clipboard)); 534 | 535 | copySelectedNodesToClipboard.on = false; 536 | } 537 | } 538 | 539 | window.addEventListener('keydown', (e) => { 540 | console.log('keydown'); 541 | console.log(e); 542 | if (e.key === 'Delete') { 543 | if (_ContentEditing.textarea) { 544 | // pass 545 | }else { 546 | if (_selected_DOM.length > 0) { 547 | applyAction({ 548 | type: 'D', 549 | node_ids: _selected_DOM.map(dom => domNode(dom).id) 550 | }); 551 | // _selected_DOM.forEach(deleteNode); 552 | // save('ids'); 553 | } 554 | } 555 | } else if (e.key === 'Enter') { 556 | if (_ContentEditing.textarea) { 557 | e.stopPropagation(); 558 | return 0; 559 | } else { 560 | if (_selected_DOM.length > 0) { 561 | // select, start editing 562 | onNodeDblClick(_selected_DOM[0]); 563 | // stop so that Enter does not overwrite the node content 564 | e.stopPropagation(); 565 | e.preventDefault(); 566 | } 567 | } 568 | } else if (e.key === 'c') { 569 | if (e.ctrlKey) { 570 | // Ctrl-C ! 571 | // moved to copy event 572 | // copySelectedNodesToClipboard(); 573 | } 574 | } else if (e.key === 'x') { 575 | if (e.ctrlKey) { 576 | // Ctrl-X 577 | // moved to cut event 578 | // copySelectedNodesToClipboard(); 579 | // _selected_DOM.forEach(deleteNode); 580 | // _selected_DOM = []; 581 | } 582 | } else if (e.key === 'v') { 583 | if (e.ctrlKey) { 584 | // Ctrl-V ! 585 | if (_ContentEditing.textarea) { 586 | // pass 587 | } else { 588 | // moved to window paste 589 | } 590 | } 591 | } else if (e.code === 'F3' || ((e.ctrlKey || e.metaKey) && e.code === 'KeyF')) { 592 | _('#search-toggle').click(); 593 | e.preventDefault(); 594 | } else if ((e.code == "KeyZ") && ( (e.ctrlKey) || (e.metaKey) )) { 595 | if (_ContentEditing.textarea){ 596 | // pass 597 | }else{ 598 | if (e.shiftKey){ 599 | // Shift - Meta - Z = Re-do on Macs 600 | _('#btnRedo').click(); 601 | }else{ 602 | // Ctrl/Meta - Z 603 | _('#btnUndo').click(); 604 | } 605 | } 606 | } else if ((e.code == "KeyY") && ( (e.ctrlKey) || (e.metaKey) )) { 607 | // Ctrl-Y was Re-do, right? 608 | _('#btnRedo').click(); 609 | } 610 | }); 611 | 612 | let __IMG = null; 613 | let __X = null; 614 | 615 | 616 | let _oldValues = null; 617 | 618 | function rotateStop(e, ui) { 619 | console.log('rotate stop'); 620 | console.log(e); 621 | console.log(ui); 622 | 623 | 624 | 625 | // ui.angle.start = ui.angle.current; 626 | applyAction({ 627 | type: 'E', 628 | node_ids: [ _Mouse.rotatingNode.id ], 629 | oldValues: _oldValues, 630 | newValues: [{ rotate: ui.angle.current }] 631 | }); 632 | 633 | _Mouse.rotatingNode.dom.style.transform = 'rotate('+ui.angle.current+'rad)'; 634 | save(_Mouse.rotatingNode); 635 | _Mouse.rotatingNode = null; 636 | } 637 | 638 | class Set{ 639 | constructor(){ 640 | this.arr = []; 641 | } 642 | 643 | } 644 | 645 | class NodeSelection{ 646 | constructor() { 647 | this.nodes = []; 648 | this.control = null; 649 | } 650 | 651 | add(node){ 652 | if (this.empty) { 653 | this.createControl(); 654 | } 655 | this.nodes.push(node); 656 | } 657 | 658 | contains(node){ 659 | return this.nodes.indexOf(node)>=0; 660 | } 661 | 662 | clear(){ 663 | this.deleteControl(); 664 | 665 | this.nodes = []; 666 | } 667 | 668 | get length() { return this.nodes.length; } 669 | get empty() { return this.nodes.length > 0; } 670 | 671 | createControl() { 672 | console.log('createControl placeholder'); 673 | } 674 | 675 | deleteControl() { 676 | console.log('deleteControl placeholder'); 677 | } 678 | } 679 | 680 | _Selection = new NodeSelection(); 681 | 682 | 683 | 684 | function selectNode (n) { 685 | console.log('select') 686 | if ( n == null) { 687 | console.log('[null]') 688 | _selected_DOM.forEach(deselectOneDOM); 689 | _selected_DOM = []; 690 | return 0; 691 | } 692 | if (!Array.isArray(n)) { 693 | log(`[${n.id}]`); 694 | n = [n]; 695 | } else { 696 | log( n.map( dom => dom.id) ); 697 | } 698 | if (_selected_DOM.length > 0) { 699 | // see how many are new ones 700 | if (n.every((dom) => dom.classList.contains('selected'))) { 701 | // if all are already selected - deselect them! 702 | n.forEach(deselectOneDOM); 703 | n.forEach((dom) => { 704 | _selected_DOM.splice(_selected_DOM.indexOf(dom)); 705 | }); 706 | }else { 707 | // if only some are already selected - add all new ones 708 | for (let dom of n) { 709 | if (dom.classList.contains('selected')) { 710 | continue; 711 | }else { 712 | selectOneDOM(dom); 713 | _selected_DOM.push(dom); 714 | } 715 | } 716 | } 717 | }else { 718 | _selected_DOM = n.slice(); 719 | _selected_DOM.forEach(selectOneDOM); 720 | } 721 | return 0; 722 | } 723 | 724 | 725 | function deselectOneDOM (dom) { 726 | dom.classList.remove('selected'); 727 | try { 728 | $(dom).rotatable('destroy'); 729 | } catch(e) { 730 | // log('error in rotatable destroy..'); 731 | // log(e); 732 | } 733 | 734 | try{ 735 | $(domNode(dom).content_dom).resizable('destroy'); 736 | } catch (e) { 737 | // console.log('some error in resizable destroy'); 738 | // console.log(e); 739 | } 740 | } 741 | 742 | 743 | function selectOneDOM (dom) { 744 | dom.classList.add('selected'); 745 | 746 | const node = domNode(dom); 747 | node.startPos = {x: node.x, y: node.y}; 748 | 749 | // https://jsfiddle.net/Twisty/7zc36sug/ 750 | // https://stackoverflow.com/a/62379454/2624911 751 | // https://jsfiddle.net/Twisty/cdLn56f1/ 752 | $(function () { 753 | console.log('init rotation, rotate node_'+node.id+'='+node.rotate) 754 | const params = { 755 | radians: node.rotate, 756 | angle: node.rotate, 757 | start: function (e, ui) { 758 | console.log('rotate start'); 759 | console.log(e); 760 | log(ui); 761 | _oldValues = [{ rotate: node.rotate }]; 762 | _Mouse.rotatingNode = node; 763 | }, 764 | stop: rotateStop 765 | }; 766 | log(params); 767 | 768 | if($(dom) 769 | .find('.ui-rotatable-handle').length>0){ 770 | }else 771 | $(node.dom).rotatable(params); 772 | 773 | $(dom) 774 | .find('.ui-rotatable-handle') 775 | .on('mouseup', function (e) { 776 | log('rotatable mouse up'); 777 | log(e); 778 | // e.stopPropagation(); 779 | }) 780 | .on('click', function(e) { 781 | log('rotatable click'); 782 | e.stopPropagation(); 783 | }) 784 | .on('dblclick', function (e) { 785 | log('rotatable dblclick'); 786 | _Mouse.rotatingNode = node; 787 | rotateStop({}, { angle: { current: 0 }}); 788 | e.stopPropagation(); 789 | }) 790 | 791 | let resizeParams = { 792 | // autoHide: true, 793 | start: function (e, ui) { 794 | console.log('resize start'); 795 | console.log(e); 796 | console.log(ui); 797 | _oldValues = [{ fontSize: node.fontSize }]; 798 | }, 799 | stop: function (e, ui) { 800 | console.log('resize stop'); 801 | console.log(e); 802 | console.log(ui); 803 | applyAction({ 804 | type:'E', 805 | node_ids:[ node.id ], 806 | oldValues: _oldValues, 807 | newValues: [{ fontSize: node.fontSize }] 808 | }) 809 | }, 810 | resize: function (e, ui) { 811 | node.fontSize = ui.size.height * _oldValues[0].fontSize / ui.originalSize.height; 812 | updateNode(node); 813 | } 814 | }; 815 | 816 | if (node.is_img){ 817 | resizeParams.aspectRatio = true; 818 | }else if(node.is_svg) { 819 | resizeParams.aspectRatio = true; 820 | } 821 | 822 | $(node.content_dom).resizable(resizeParams); 823 | $(dom) 824 | .find('.ui-resizable-handle') 825 | .on('mousedown', function (e) { 826 | console.log('resize mouse down'); 827 | // e.stopPropagation(); 828 | _Mouse.is.resizing = true; 829 | }) 830 | .on('mouseup', function (e) { 831 | console.log('resize mouse up'); 832 | // e.stopPropagation(); 833 | }); 834 | }); 835 | } 836 | 837 | function isMenuShown(){ 838 | return !_('#wrapper').classList.contains('toggled'); 839 | } 840 | 841 | function showMenu(){ 842 | if(!isMenuShown()){ 843 | _('#menu-toggle').click(); 844 | } 845 | } 846 | 847 | // :::: ::: :::::::: ::::::::: :::::::::: :::::::::: ::: ::: :::: ::: :::::::: :::::::: 848 | // :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:+: :+: :+: :+: :+: :+: 849 | // :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ +:+ +:+ 850 | // +#+ +:+ +#+ +#+ +:+ +#+ +:+ +#++:++# :#::+::# +#+ +:+ +#+ +:+ +#+ +#+ +#++:++#++ 851 | // +#+ +#+#+# +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+# +#+ +#+ 852 | // #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+# #+# #+# #+# #+# 853 | // ### #### ######## ######### ########## ### ######## ### #### ######## ######## 854 | 855 | let _ContentEditing = { 856 | dom: null, 857 | textarea: null, 858 | stop: function(){ 859 | log('_ContentEditing.stop'); 860 | let tdom = _ContentEditing.textarea.parentElement.parentElement; 861 | _ContentEditing.textarea.parentElement.removeChild(_ContentEditing.textarea); 862 | const A = { type: 'D', node_ids: [ domNode(tdom).id ] }; 863 | if (_ContentEditing.textarea.value === '') { 864 | //deleteNode(tdom); 865 | _ContentEditing.dom = null; 866 | }else { 867 | A.type = 'E'; 868 | A.oldValues = [ { text: domNode(tdom).startText } ]; 869 | A.newValues = [ { text: domNode(tdom).text } ]; 870 | // A.nodes = [ domNode(tdom) ]; 871 | // newNode(tdom); 872 | } 873 | applyAction(A); 874 | _ContentEditing.textarea = null; 875 | }, 876 | start: function(node){ 877 | 878 | }, 879 | isInProgress: function nodeEditInProgress() { 880 | return _ContentEditing.dom; 881 | }, 882 | } 883 | 884 | // let _ContentEditing.dom = null; 885 | // let _ContentEditing.textarea = null; 886 | 887 | function textareaAutoResize (e) { 888 | var target = ('style' in e) ? e : this; 889 | target.style.height = 'auto'; 890 | target.style.height = target.scrollHeight.toPx(); 891 | } 892 | 893 | function textareaBtnDown (e) { 894 | log('textareaBtnDown'); 895 | log(e); 896 | if ((e.ctrlKey) && ((e.keyCode === 0xA) || (e.keyCode === 0xD))) { 897 | log('Ctrl+Enter!'); 898 | _ContentEditing.stop(); 899 | e.stopPropagation(); 900 | 901 | } 902 | if ((e.key === 'Enter') && (e.shiftKey)) { 903 | log('Shift-enter'); 904 | const tnode_orig = domNode(_ContentEditing.dom.parentElement); 905 | const tnode = stripNode(tnode_orig); 906 | let th = _ContentEditing.dom.getBoundingClientRect().height; 907 | 908 | _ContentEditing.stop(); 909 | 910 | if (_ContentEditing.isInProgress()) { 911 | // if the node has not been deleted (as empty) 912 | // , get actual height (not height of markdown textarea) 913 | th = tnode_orig.content_dom.getBoundingClientRect().height; 914 | } 915 | 916 | applyAction({ 917 | type:'A', 918 | nodes: [{ 919 | x: 1 * tnode.x, 920 | y: 1 * tnode.y + 921 | // + 1*tnode['fontSize'] 922 | (th / _View.state.S), 923 | text: '', 924 | fontSize: tnode.fontSize 925 | }] 926 | }) 927 | 928 | setTimeout(function(){ 929 | selectNode(null); 930 | 931 | console.log(_NODES[_NODES.length - 1].dom) 932 | selectNode(_NODES[_NODES.length - 1].dom); 933 | 934 | onNodeDblClick(_NODES[_NODES.length - 1].dom); 935 | 936 | // neither preventDefault nor stopPropagation 937 | // stoped newline from appearing 938 | setTimeout(function () { 939 | _ContentEditing.textarea.value = ''; 940 | _ContentEditing.textarea.focus(); 941 | }, 10); 942 | },10); 943 | 944 | e.stopPropagation(); 945 | 946 | } 947 | 948 | if (e.key === 'Tab') { 949 | // Tab 950 | 951 | // no jump-to-next-field 952 | e.preventDefault(); 953 | } 954 | 955 | // https://stackoverflow.com/a/3369624/2624911 956 | if (_ContentEditing.isInProgress()) { 957 | if (e.key === 'Escape') { // escape key maps to keycode `27` 958 | log('Escape!'); 959 | 960 | selectNode(_ContentEditing.dom.parentElement); 961 | 962 | _ContentEditing.stop(); 963 | 964 | e.stopPropagation(); 965 | } 966 | } 967 | } 968 | 969 | function onNodeDblClick (e) { 970 | if ('preventDefault' in e) { 971 | _ContentEditing.dom = this; 972 | } else { 973 | _ContentEditing.dom = e; 974 | } 975 | 976 | if ( (!domNode(_ContentEditing.dom).is_svg) 977 | ||((domNode(_ContentEditing.dom).is_svg) 978 | &&(onNodeDblClick.path_ok))) { 979 | 980 | if ('preventDefault' in e) { 981 | e.preventDefault(); 982 | e.stopPropagation(); 983 | } 984 | console.log('double-clicked on [' + _ContentEditing.dom.id + '] : ' + _ContentEditing.dom.innerText); 985 | console.log(_ContentEditing.dom); 986 | 987 | const node = domNode(_ContentEditing.dom); 988 | node.startText = node.text; 989 | 990 | _ContentEditing.dom = _ContentEditing.dom.getElementsByClassName('np-n-c')[0]; 991 | 992 | ta = document.createElement('textarea'); 993 | ta.id = 'ta'; 994 | ta.value = node.text; 995 | 996 | ta.dataset.initS = _View.state.S; 997 | ta.dataset.initWidth = Math.max(width / 3, _ContentEditing.dom.getBoundingClientRect().width + 20); 998 | ta.dataset.initHeight = _ContentEditing.dom.getBoundingClientRect().height; 999 | ta.style.width = ta.dataset.initWidth.toPx(); 1000 | ta.style.height = ta.dataset['initHeight'].toPx(); 1001 | 1002 | ta.onkeydown = textareaBtnDown; 1003 | ta.onkeyup = textareaAutoResize; 1004 | ta.oninput = function (e) { 1005 | console.log('ta input'); 1006 | domNode(this.parentElement.parentElement).text = this.value; 1007 | }; 1008 | 1009 | ta.onmousedown = (e) => { 1010 | if (e.button === 1) { 1011 | // drag on middle button => just pass the event 1012 | } else { 1013 | _Mouse.is.downContentEdit = true; 1014 | } 1015 | }; 1016 | 1017 | _ContentEditing.dom.innerHTML = ''; 1018 | _ContentEditing.dom.appendChild(ta); 1019 | 1020 | ta.select(); 1021 | _ContentEditing.textarea = ta; 1022 | 1023 | } 1024 | onNodeDblClick.path_ok = false; 1025 | } 1026 | 1027 | 1028 | function onNodeMouseDown (e) { 1029 | console.log('onNodeMouseBtn'); 1030 | console.log(this.id); 1031 | 1032 | if((!domNode(this).is_svg)||((domNode(this).is_svg)&&(onNodeMouseDown.path_ok))){ 1033 | _Mouse.down.node = domNode(this); 1034 | // console.log(e); 1035 | if (e.button === 1) { 1036 | _Mouse.drag.node = _Mouse.down.node; 1037 | _Mouse.drag.start = [e.clientX, e.clientY]; 1038 | 1039 | let applyDrag = [ this ]; 1040 | if (this.classList.contains('selected')) { 1041 | // save all selected positions 1042 | applyDrag = _selected_DOM; 1043 | } 1044 | applyDrag.forEach((dom) => { 1045 | const node = domNode(dom); 1046 | node.startPos = { x: node.x, y: node.y }; 1047 | }); 1048 | 1049 | e.preventDefault(); 1050 | } else if (e.button === 0) { 1051 | // left mouse button 1052 | console.log(e); 1053 | } else { 1054 | // pass 1055 | } 1056 | } 1057 | if (e.ctrlKey) { 1058 | e.preventDefault(); 1059 | } 1060 | 1061 | onNodeMouseDown.path_ok=false; 1062 | } 1063 | 1064 | function onNodeClick (e) { 1065 | console.log('clicked on [' + this.id + '] : ' + this.innerText); 1066 | 1067 | if((!domNode(this).is_svg)||((domNode(this).is_svg)&&(onNodeClick.path_ok))){ 1068 | 1069 | if (e.shiftKey) { 1070 | // multiselect! 1071 | // selectNode(this); //already handled via drag-select 1072 | // e.stopPropagation(); 1073 | } else { 1074 | selectNode(null); 1075 | selectNode(this); 1076 | } 1077 | 1078 | } 1079 | onNodeClick.path_ok = false; 1080 | } 1081 | 1082 | function changeStrokeWidth(delta=1){ 1083 | const node_ids = []; 1084 | const newValues = []; 1085 | 1086 | const k = Math.pow(1.25, delta); 1087 | 1088 | _selected_DOM.forEach((dom) => { 1089 | const node = domNode(dom); 1090 | if (node.is_svg == false) { 1091 | return 0; 1092 | } 1093 | 1094 | node_ids.push(node.id); 1095 | newValues.push( { 1096 | 'style.strokeWidth': (node.style.strokeWidth || 1) * k 1097 | }); 1098 | }) 1099 | 1100 | applyAction( { 1101 | type: 'E', 1102 | node_ids: node_ids, 1103 | newValues: newValues 1104 | }) 1105 | } 1106 | 1107 | function updateNode (node_) { 1108 | let node = node_; 1109 | if (node.hasOwnProperty('dom')) { 1110 | dom = node.dom; 1111 | } else { 1112 | dom = node_; 1113 | node = domNode(dom); 1114 | } 1115 | 1116 | var pos = _View.posToClient(node.x, node.y); 1117 | dom.style.left = pos[0].toPx(); 1118 | dom.style.top = pos[1].toPx(); 1119 | dom.style.fontSize = (node.fontSize * _View.state.S).toPx(); 1120 | 1121 | dom.style.zIndex = Math.floor(200 - 10 * Math.log((node.fontSize) * _View.state.S )); 1122 | 1123 | let k = ( _View.state.S * node.fontSize / 20); 1124 | 1125 | if(node.is_svg){ 1126 | node.content_dom.style.position = 'relative'; 1127 | node.content_dom.style.left = '0px'; 1128 | node.content_dom.style.top = '0px'; 1129 | 1130 | let ow = node.svg_dom.getAttribute('width')*1; 1131 | let oh = node.svg_dom.getAttribute('height')*1 1132 | node.content_dom.style.width = (ow * k).toPx(); 1133 | node.content_dom.style.height = (oh * k).toPx(); 1134 | node.dom.style.height = (oh * k).toPx(); 1135 | node.dom.style.width = (ow * k).toPx(); 1136 | node.svg_dom.style.transform = 'translate(-'+ow/2+'px,-'+oh/2+'px) scale(' + k + ')' +' translate('+ow/2+'px,'+oh/2+'px)';// + ' rotate('+node.rotate+'rad) ' ; 1137 | } 1138 | 1139 | if(node.is_img){ 1140 | if(node.size){ 1141 | const h = 5 * (node.fontSize) * _View.state.S; 1142 | const w = h * node.size[0] / node.size[1]; 1143 | node.content_dom.style.width = w.toPx(); 1144 | node.content_dom.style.height = h.toPx(); 1145 | node.img_dom.style.width = w.toPx(); 1146 | node.img_dom.style.height = h.toPx(); 1147 | } 1148 | }else{ 1149 | dom.getElementsByTagName('img').forEach( (e) => { 1150 | e.style.width = 'auto'; 1151 | e.style.height = (100 * k).toPx(); 1152 | }); 1153 | } 1154 | 1155 | if (node.rotate !== 0) { 1156 | if(dom.classList.contains('selected')){ 1157 | 1158 | }else{ 1159 | dom.style.transform = 'rotate(' + node.rotate + 'rad)'; 1160 | } 1161 | } 1162 | } 1163 | 1164 | function updateSizes () { 1165 | if (!('state' in updateSizes)) { 1166 | updateSizes.state = ''; 1167 | } 1168 | const state = JSON.stringify(_View.state); 1169 | if (state === updateSizes.state) { 1170 | // pass 1171 | } else { 1172 | for (let j = 0; j < _NODES.length; j++) { 1173 | if (_NODES[j].vis) { 1174 | const d = _NODES[j]; 1175 | d.xMax = d.x + d.dom.clientWidth / _View.state.S; 1176 | d.yMax = d.y + d.dom.clientHeight / _View.state.S; 1177 | } 1178 | } 1179 | updateSizes.state = state; 1180 | } 1181 | 1182 | setTimeout(updateSizes, 100); 1183 | } 1184 | 1185 | function calcBox (d) { 1186 | if (d.text.indexOf('![') >= 0) { 1187 | d.xMax = d.x + d.fontSize * 10; 1188 | d.yMax = d.y + d.fontSize * 5; 1189 | } else { 1190 | d.xMax = d.x + d.text.length * d.fontSize * 0.5; 1191 | d.yMax = d.y + d.text.split('\n').length * d.fontSize; 1192 | } 1193 | } 1194 | 1195 | function isVisible (d) { 1196 | if (!('xMax' in d)) { 1197 | calcBox(d); 1198 | } 1199 | return ( 1200 | (d.fontSize > 0.2 / _View.state.S) 1201 | && _View.isBoxSeen(d.x, d.xMax, d.y, d.yMax) 1202 | ); 1203 | } 1204 | 1205 | function calcVisible (d, onhide, onshow) { 1206 | const newVis = isVisible(d); 1207 | 1208 | if (!('vis' in d)) { 1209 | if (newVis) { 1210 | onshow(d); 1211 | } else { 1212 | onhide(d); 1213 | } 1214 | } else { 1215 | if (d.vis) { 1216 | if (newVis) { 1217 | // 1218 | } else { 1219 | // console.log('hiding:'); 1220 | // console.log(d); 1221 | onhide(d); 1222 | } 1223 | } else { 1224 | if (newVis) { 1225 | // console.log('show:'); 1226 | // console.log(d); 1227 | onshow(d); 1228 | } else { 1229 | // did not show before and not showing now, pass 1230 | } 1231 | } 1232 | } 1233 | d.vis = newVis; 1234 | } 1235 | 1236 | function redrawNode (e) { 1237 | if (!e.hasOwnProperty('deleted')) { 1238 | e.deleted = false; 1239 | } 1240 | if (e.deleted) { 1241 | e.dom.style.display = 'none'; 1242 | } else { 1243 | e.dom.style.display = ''; 1244 | calcVisible(e 1245 | , function () { // onhide 1246 | e.dom.style.opacity = 0; 1247 | e.dom.style.display = 'none'; 1248 | }, function () { // onshow 1249 | let oldTD = 0; 1250 | if ('node' in e) { 1251 | oldTD = e.dom.style.transitionDuration.slice(0, -1) * 1; 1252 | e.dom.style.display = 'none'; 1253 | } 1254 | updateNode(e); 1255 | setTimeout(function () { 1256 | e.dom.style.display = ''; 1257 | e.dom.style.opacity = 1; 1258 | }, 1 + 1000 * oldTD); 1259 | } 1260 | ); 1261 | } 1262 | } 1263 | 1264 | // function redrawAllNodes(){ 1265 | // if(redrawAllNodes.running){ 1266 | // redrawAllNodes.waiting = true; 1267 | // }else{ 1268 | // redrawAllNodes.running = true; 1269 | // _NODES.forEach(redrawNode); 1270 | 1271 | // if(redrawAllNodes.waiting){ 1272 | // redrawAllNodes.waiting = false; 1273 | // setTimeout(redrawAllNodes,5); 1274 | // }else{ 1275 | // redrawAllNodes.running=false; 1276 | // } 1277 | // } 1278 | // // clearTimeout(redrawAllNodes.timeout); 1279 | // // redrawAllNodes.timeout = setTimeout(function(){ 1280 | // // }, 1); 1281 | // } 1282 | // redrawAllNodes.running=false; 1283 | // redrawAllNodes.waiting=false; 1284 | 1285 | function redraw () { 1286 | console.log('redraw'); 1287 | _NODES.forEach((e) => { 1288 | if (('vis' in e) && (e.vis)) { 1289 | // console.log('vis => update'); 1290 | // console.log(e); 1291 | updateNode(e); 1292 | } 1293 | }); 1294 | 1295 | // setTimeout(function(){redrawAllNodes();},10); 1296 | setTimeout(function () { 1297 | _NODES.forEach(redrawNode); 1298 | }, 5); 1299 | 1300 | // _NODES.forEach(redrawNode); 1301 | 1302 | if (_ContentEditing.textarea) { 1303 | console.log('redraw : contextEditTextarea'); 1304 | console.log('initWidth:'); 1305 | console.log(_ContentEditing.textarea.dataset.initWidth); 1306 | console.log('initS: ' + _ContentEditing.textarea.dataset.initS); 1307 | console.log('_ContentEditing.textarea.style.width = ' + _ContentEditing.textarea.style.width); 1308 | _ContentEditing.textarea.style.width = 1 * _ContentEditing.textarea.dataset.initWidth * _View.state.S / (1 * _ContentEditing.textarea.dataset.initS).toPx(); 1309 | console.log('_ContentEditing.textarea.style.width = ' + _ContentEditing.textarea.style.width); 1310 | _ContentEditing.textarea.style.height = _ContentEditing.textarea.dataset.initHeight * _View.state.S / _ContentEditing.textarea.dataset.initS.toPx(); 1311 | } 1312 | 1313 | if (_selected_DOM.length > 0) { 1314 | _selected_DOM.forEach(dom => { 1315 | let wrapper = dom.getElementsByClassName('ui-wrapper'); 1316 | 1317 | if (wrapper.length > 0) { 1318 | wrapper = wrapper[0]; 1319 | const img = wrapper.getElementsByTagName('img')[0]; 1320 | 1321 | wrapper.style.width = (wrapper.style.width.pxToFloat() * _View.state.S / img.dataset.origS).toPx(); 1322 | wrapper.style.height = (wrapper.style.height.pxToFloat() * _View.state.S / img.dataset.origS).toPx(); 1323 | img.dataset.origS = _View.state.S; 1324 | } 1325 | }); 1326 | } 1327 | } 1328 | 1329 | function getHTML (node) { 1330 | if((node.text.slice(-4)=='svg>') 1331 | && (node.text.slice(0,4)=='') 1336 | && (node.text.slice(0,4)=='') 1346 | .replace(/href="(\?[^"]+)"/, /class="local" onclick="zoomToURL('$1',false)"/); 1347 | // .replaceAll(/(href="[^\?])/g,'onclick="(e)=>{console.log(e);e.stopPropagation();}" $1'); 1348 | } 1349 | } 1350 | 1351 | function isCurrentState () { 1352 | return ((history.state) 1353 | && ('T' in history.state) 1354 | && ('S' in history.state) 1355 | && (history.state.T[0] == _View.state.T[0]) 1356 | && (history.state.T[1] == _View.state.T[1]) 1357 | && (history.state.S == _View.state.S)); 1358 | } 1359 | 1360 | function replaceHistoryState () { 1361 | const url = window.location.href.indexOf('?') == -1 1362 | ? window.location.href 1363 | : window.location.href.slice(0, window.location.href.indexOf('?')); 1364 | window.history.replaceState( 1365 | _View.state, 1366 | 'Noteplace', 1367 | url + _View.getStateURL()); 1368 | console.log('history replaced'); 1369 | } 1370 | 1371 | // const zoom_urlReplaceTimeout = setInterval(function () { 1372 | // if (!isCurrentState()) { 1373 | // replaceHistoryState(); 1374 | // } 1375 | // }, 200); 1376 | 1377 | function onFontSizeEdit () { 1378 | if (_selected_DOM.length === 1) { 1379 | const dom = _selected_DOM[0]; 1380 | const node = domNode(dom); 1381 | node.fontSize = _('#fontSize').value; 1382 | _selected_DOM[0].classList.add('zoom'); 1383 | 1384 | updateNode(_selected_DOM[0]); 1385 | // _selected_DOM.classList.remove('zoom'); 1386 | 1387 | _('#fontSize').step = _('#fontSize').value * 0.25; 1388 | 1389 | save(_selected_DOM[0]); 1390 | } 1391 | } 1392 | 1393 | function onTextEditChange () { 1394 | if (_selected_DOM !== null) { 1395 | _selected_DOM.dataset.text = this.value; 1396 | newNode(_selected_DOM); 1397 | 1398 | save(_selected_DOM); 1399 | } 1400 | } 1401 | 1402 | function addRandomNodes (N, Xlim, Ylim, FSLim) { 1403 | for (let j = 0; j < N; j++) { 1404 | const id = newNodeID(); 1405 | newNode({ 1406 | x: Xlim[0] + Math.random() * (Xlim[1] - Xlim[0]), 1407 | y: Ylim[0] + Math.random() * (Ylim[1] - Ylim[0]), 1408 | fontSize: FSLim[0] + Math.random() * (FSLim[1] - FSLim[0]), 1409 | text: 'test__' + id, 1410 | id: id, 1411 | rotate: Math.random() * 2 * Math.PI 1412 | }); 1413 | } 1414 | redraw(); 1415 | } 1416 | 1417 | function editFontSize (delta) { 1418 | if (_selected_DOM.length > 0) { 1419 | if (_selected_DOM.length == 0) { 1420 | // one element selected, fontSize input is related to it 1421 | _('#fontSize').value *= Math.pow(1.25, delta); 1422 | onFontSizeEdit(); 1423 | } else { 1424 | const k = Math.pow(1.25, delta); 1425 | 1426 | const centerPos = calcCenterPos(_selected_DOM.map(domNode)); 1427 | 1428 | applyAction({ 1429 | type: 'E', 1430 | node_ids: _selected_DOM.map( dom => domNode(dom).id ), 1431 | newValues: _selected_DOM.map( dom => { 1432 | let node = domNode(dom); 1433 | return { 1434 | fontSize: node.fontSize * k, 1435 | x: centerPos[0] + (node.x - centerPos[0]) * k, 1436 | y: centerPos[1] + (node.y - centerPos[1]) * k 1437 | }; }) 1438 | }) 1439 | 1440 | _selected_DOM.forEach((dom) => { 1441 | // let node = domNode(dom); 1442 | // node.fontSize *= k; 1443 | // node.x = centerPos[0] + (node.x - centerPos[0]) * k; 1444 | // node.y = centerPos[1] + (node.y - centerPos[1]) * k; 1445 | updateNode(dom); 1446 | 1447 | let wrapper = dom.getElementsByClassName('ui-wrapper'); 1448 | if (wrapper.length > 0) { 1449 | wrapper = wrapper[0]; 1450 | console.log(wrapper); 1451 | wrapper.style.width = (wrapper.style.width.slice(0, -2) * k).toPx(); 1452 | wrapper.style.height = (wrapper.style.height.slice(0, -2) * k).toPx(); 1453 | } 1454 | }); 1455 | } 1456 | } 1457 | } 1458 | 1459 | function showModalYesNo (title, body, yes_callback) { 1460 | _('#modalYesNoLabel').innerHTML = title; 1461 | _('#modalYesNoBody').innerHTML = body; 1462 | _('#modalYesNo-Yes').onclick = yes_callback; 1463 | $('#modalYesNo').modal('show'); 1464 | } 1465 | 1466 | // :::::::: ::::::::::: ::: ::::::::: ::::::::::: ::: ::: ::::::::: 1467 | // :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 1468 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 1469 | // +#++:++#++ +#+ +#++:++#++: +#++:++#: +#+ +#+ +:+ +#++:++#+ 1470 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 1471 | // #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 1472 | // ######## ### ### ### ### ### ### ######## ### 1473 | 1474 | node_container.dataset.x = 0; 1475 | node_container.dataset.y = 0; 1476 | 1477 | // event handlers 1478 | 1479 | _('#text').oninput = onTextEditChange; 1480 | _('#fontSize').onchange = onFontSizeEdit; 1481 | 1482 | _('#btnAddLots').onclick = function () { 1483 | addRandomNodesToView(1 * _('#number').value); 1484 | }; 1485 | 1486 | _('#btnZoomIn').onclick = function () { 1487 | zoomInOut(1); 1488 | }; 1489 | _('#btnZoomOut').onclick = function () { 1490 | zoomInOut(-1); 1491 | }; 1492 | 1493 | _('#btnFontMinus').onclick = function () { 1494 | editFontSize(-1); 1495 | }; 1496 | _('#btnFontPlus').onclick = function () { 1497 | editFontSize(+1); 1498 | }; 1499 | 1500 | // Load nodes? 1501 | 1502 | if (_('.node').length) { 1503 | console.log('seems we already have nodes in HTML.'); 1504 | 1505 | _localStorage.save(); 1506 | } else { 1507 | console.log('No nodes in html..'); 1508 | let nodes = []; 1509 | let nodeIDs = []; 1510 | try { 1511 | nodeIDs = _localStorage.nodeIDs.load(); 1512 | nodes = nodeIDs.map(_localStorage.node.load); 1513 | }catch (e) { 1514 | console.trace(e); 1515 | console.log('no nodes in localStorage, loading default'); 1516 | nodes = nodes_default; 1517 | } 1518 | nodes.forEach(n => { 1519 | newNode(stripNode(n)); 1520 | }); 1521 | } 1522 | 1523 | try { 1524 | _PLACES = _localStorage.places.load(); 1525 | }catch (e) { 1526 | _PLACES = stripPlace(_PLACES_default); 1527 | } 1528 | 1529 | fillPlaces(); 1530 | 1531 | _localStorage.save(); 1532 | 1533 | $('#exampleModal').on('shown.bs.modal', function () { 1534 | $('#modal-input').trigger('focus'); 1535 | }); 1536 | 1537 | const copydiv = _ce('div' 1538 | , 'id', 'copydiv' 1539 | , 'contentEditable', 'true' 1540 | , 'onready', function () { copydiv.focus() ;} 1541 | ); 1542 | let copydiv_observer = null; 1543 | node_container.appendChild(copydiv); 1544 | 1545 | window.addEventListener('paste', function (e) { 1546 | console.log('window paste'); 1547 | console.log(e); 1548 | 1549 | if (_ContentEditing.textarea) { 1550 | // pass 1551 | } else { 1552 | // hmm, pasting something from the outside? 1553 | copydiv.focus(); 1554 | 1555 | if (1) { 1556 | copydiv_observer = addOnContentChange(copydiv, function (e) { 1557 | console.log('copydiv paste!'); 1558 | console.log(e); 1559 | 1560 | E = e; 1561 | 1562 | let tstuff = e.map( 1563 | je => je.addedNodes.map( 1564 | (n) => 'innerHTML' in n ? n.innerHTML:n.textContent 1565 | ).join('') 1566 | ).join(''); 1567 | 1568 | setTimeout(function () { 1569 | tstuff = _('#copydiv').innerHTML; 1570 | 1571 | copydiv_observer.disconnect(); 1572 | copydiv.innerHTML = ''; 1573 | 1574 | let json_parsed = false; 1575 | if (tstuff[0] == '[') { 1576 | try { 1577 | _Mouse.clipboard = JSON.parse( 1578 | // I know, right? 1579 | // why does pasting JSON-encoded html 1580 | // create this sort of nonsense? 1581 | tstuff.replaceAll('<', '<') 1582 | .replaceAll('>', '>') 1583 | .replaceAll('&', '&') 1584 | // .replaceAll(''',"'") 1585 | // .replaceAll('"','"') 1586 | ); 1587 | json_parsed = true; 1588 | }catch (e) { 1589 | json_parsed = false; 1590 | } 1591 | } 1592 | 1593 | if (json_parsed) { 1594 | console.log('Pasting JSON-parsed _Mouse.clipboard'); 1595 | selectNode(null); 1596 | // paste Under the cursor? 1597 | 1598 | const A = { type: 'A', nodes: [] }; 1599 | 1600 | _Mouse.clipboard.forEach((node) => { 1601 | const nnode = stripNode(node); 1602 | // de-duplicate if 1603 | nnode.id = newNodeID(nnode.id); 1604 | nnode.x /= _View.state.S; 1605 | nnode.y /= _View.state.S; 1606 | nnode.fontSize /= _View.state.S; 1607 | const tmousePos = clientToPos(_Mouse.pos); 1608 | nnode.x += tmousePos[0]; 1609 | nnode.y += tmousePos[1]; 1610 | 1611 | A.nodes.push(nnode); 1612 | 1613 | // selectNode(newNode(nnode)); 1614 | }); 1615 | 1616 | const h = applyAction(A); 1617 | 1618 | selectNode(h.node_ids.map( id => idNode(id).dom )); 1619 | 1620 | }else { 1621 | // I know this is not perfect (HAHAHAHAHA!!...) 1622 | // but it kinda works 1623 | tstuff = tstuff.replaceAll(/font-size:[ 0-9]+(px)?;?/g, ''); 1624 | tstuff = tstuff.replaceAll(/width:[ 0-9]+(px)?;?/g, ''); 1625 | tstuff = tstuff.replaceAll(/line-height:[ 0-9]+(px)?;?/g, ''); 1626 | tstuff = tstuff.replaceAll(/height:[ 0-9]+(px)?;?/g, ''); 1627 | 1628 | console.log(tstuff); 1629 | 1630 | applyAction({ 1631 | type: 'A', 1632 | nodes: [{ 1633 | text: tstuff 1634 | }] 1635 | }); 1636 | selectNode(_NODES[_NODES.length - 1].dom); 1637 | } 1638 | 1639 | }, 50); 1640 | }); 1641 | } 1642 | } 1643 | }); 1644 | 1645 | window.addEventListener('cut', function (e) { 1646 | console.log('window cut'); 1647 | console.log(e); 1648 | if (_ContentEditing.textarea) { 1649 | // pass 1650 | } else { 1651 | e.stopPropagation(); 1652 | e.preventDefault(); 1653 | copySelectedNodesToClipboard(); 1654 | _selected_DOM.forEach(deleteNode); 1655 | _selected_DOM = []; 1656 | } 1657 | }); 1658 | 1659 | window.addEventListener('copy', function (e) { 1660 | console.log('window copy'); 1661 | console.log(e); 1662 | if (_ContentEditing.textarea) { 1663 | // pass 1664 | } else { 1665 | copySelectedNodesToClipboard(); 1666 | } 1667 | }); 1668 | 1669 | _('#btnClear').addEventListener('click', function () { 1670 | showModalYesNo( 1671 | 'Clear everything?', 1672 | 'Are you sure you want to clear everything?', 1673 | function () { 1674 | _RESTART([]); 1675 | } 1676 | ); 1677 | }); 1678 | 1679 | _('#btnRestart').addEventListener('click', function () { 1680 | showModalYesNo( 1681 | 'Restart?', 1682 | 'Are you sure you want to discard everything and restart?', 1683 | function () { 1684 | _RESTART(); 1685 | } 1686 | ); 1687 | }); 1688 | 1689 | let _fileList = null; 1690 | container.addEventListener('drop', function (e) { 1691 | console.log('container drop'); 1692 | console.log(e); 1693 | e.stopPropagation(); 1694 | e.preventDefault(); 1695 | 1696 | container.classList.remove('drag-hover'); 1697 | 1698 | if (e.dataTransfer.files.length > 0) { 1699 | _fileList = e.dataTransfer.files; 1700 | console.log(_fileList); 1701 | // _fileList 1702 | } else { 1703 | selectNode(null); 1704 | let h = applyAction( { 1705 | type: 'A', 1706 | nodes: [{ 1707 | text: e.dataTransfer.getData('text'), 1708 | mousePos: [e.clientX, e.clientY] 1709 | }] 1710 | } ); 1711 | selectNode(_NODES[_NODES.length-1].dom); 1712 | // selectNode( 1713 | // newNode({ 1714 | // text: e.dataTransfer.getData('text'), 1715 | // mousePos: [e.clientX, e.clientY] 1716 | // }) 1717 | // ); 1718 | } 1719 | }); 1720 | 1721 | container.addEventListener('dragover', function (e) { 1722 | // console.log('container dragover'); 1723 | // console.log(e); 1724 | e.preventDefault(); 1725 | }); 1726 | 1727 | 1728 | container.addEventListener('dragenter', function (e) { 1729 | console.log('container dragenter'); 1730 | console.log(e); 1731 | log( e.dataTransfer.getData('text')) 1732 | 1733 | container.classList.add('drag-hover'); 1734 | 1735 | if(_previewNode==null){ 1736 | _previewNode = newNode({ 1737 | text: _PLACES_dragContent || "", 1738 | mousePos: [e.clientX, e.clientY] 1739 | }, true, true); 1740 | 1741 | 1742 | _previewNode.onmousemove = function (e) { 1743 | if(_previewNode){ 1744 | _previewNode.style.left = e.clientX-1; 1745 | _previewNode.style.top = e.clientY-1; 1746 | } 1747 | } 1748 | _previewNode.ondragover = function (e) { 1749 | log('previewNode dragover'); 1750 | } 1751 | 1752 | console.log(_previewNode); 1753 | } else{ 1754 | log('already previewing') 1755 | } 1756 | e.preventDefault(); 1757 | }); 1758 | 1759 | container.addEventListener('dragleave', function (e) { 1760 | console.log('container dragleave'); 1761 | container.classList.remove('drag-hover'); 1762 | // console.log(e); 1763 | 1764 | // node_container.removeChild(_previewNode); 1765 | // _previewNode = null; 1766 | 1767 | e.preventDefault(); 1768 | }); 1769 | 1770 | container.addEventListener('dragend', function (e) { 1771 | console.log('container dragend'); 1772 | // node_container.removeChild(_previewNode); 1773 | // _previewNode = null; 1774 | // console.log(e); 1775 | }); 1776 | 1777 | 1778 | // start updateSizes process 1779 | updateSizes(); 1780 | 1781 | _('#btnPaletteToggle').onclick = function () { 1782 | if (_('#btnPaletteToggle').ariaExpanded === 'true') { 1783 | _('#btnPaletteToggle').innerHTML = ''; 1784 | }else { 1785 | _('#btnPaletteToggle').innerHTML = ''; 1786 | randomizePalette(); 1787 | } 1788 | }; 1789 | 1790 | $('#menu-toggle').click(function (e) { 1791 | e.preventDefault(); 1792 | $('#wrapper').toggleClass('toggled'); 1793 | }); 1794 | 1795 | var toastElList = [].slice.call(document.querySelectorAll('.toast')) 1796 | var toastList = toastElList.map(function (toastEl) { 1797 | return new bootstrap.Toast(toastEl) 1798 | }) 1799 | 1800 | function toast(content) { 1801 | _('.toast-body')[0].innerHTML = content; 1802 | toastList[0].show(); 1803 | } 1804 | 1805 | window.onpopstate = function (e) { 1806 | e.stopPropagation(); 1807 | e.preventDefault(); 1808 | console.log('location: ' + document.location + ', state: ' + JSON.stringify(e.state)); 1809 | _View.goto(e.state); 1810 | }; 1811 | 1812 | // applyZoom(T,S, false); 1813 | _View.gotoURL(window.location.search, false); 1814 | 1815 | // ::: ::: ::::::::::: ::::::::::: ::: ::::::::::: ::::::::::: ::: ::: 1816 | // :+: :+: :+: :+: :+: :+: :+: :+: :+: 1817 | // +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 1818 | // +#+ +:+ +#+ +#+ +#+ +#+ +#+ +#++: 1819 | // +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 1820 | // #+# #+# #+# #+# #+# #+# #+# #+# 1821 | // ######## ### ########### ########## ########### ### ### 1822 | 1823 | function status (...arguments) { 1824 | var s = arguments.map(toStr).join(', '); 1825 | if ((arguments.length === 1) && (typeof (arguments[0]) === 'object')) { 1826 | s = s.slice(1, s.length - 1); 1827 | } 1828 | console.log(s); 1829 | _('#status').innerText = s 1830 | } 1831 | 1832 | function addRandomNodesToView (N) { 1833 | addRandomNodes( 1834 | 1 * N, 1835 | [_View.state.T[0], _View.state.T[0] + width / _View.state.S], 1836 | [_View.state.T[1], _View.state.T[1] + height / _View.state.S], 1837 | [0.02 / _View.state.S, 20 / _View.state.S] 1838 | ); 1839 | } 1840 | 1841 | function _RESTART (new_nodes = nodes_default, new_places = _PLACES_default, new_links=[]) { 1842 | console.log('_RESTART'); 1843 | console.log('new_nodes=[' + new_nodes + ']'); 1844 | console.log('new_places=[' + new_places + ']'); 1845 | log('new_links=[' + new_links + ']'); 1846 | // 1847 | _NODES = []; 1848 | newNodeID.N = 0; 1849 | $('.node').remove(); 1850 | // 1851 | gen_DOMId2nodej(); _PLACES = stripPlace(_PLACES_default); 1852 | fillPlaces(); 1853 | // 1854 | new_nodes.forEach(n => { 1855 | newNode(stripNode(n)); 1856 | }); 1857 | // 1858 | _HISTORY = []; 1859 | _HISTORY_CURRENT_ID = null; 1860 | genHistIDMap(); 1861 | fillHistoryList(); 1862 | // 1863 | console.log('restart'); 1864 | _View.applyZoom([0, 0], 1, false, false); 1865 | // 1866 | __GDRIVE_saveFilename = null; 1867 | __GDRIVE_savedID = null; 1868 | } 1869 | 1870 | _('#btnSaveFast').onclick = function (e) { 1871 | _localStorage.save(); 1872 | if ( __GDRIVE_savedID != null ){ 1873 | gdriveRewrite(__GDRIVE_saveFilename, __GDRIVE_savedID); 1874 | } 1875 | e.stopPropagation(); 1876 | }; 1877 | 1878 | 1879 | 1880 | fillHistoryList(); 1881 | --------------------------------------------------------------------------------