From 2575eaacbf8b280d0f914d6f1109e78222aae254 Mon Sep 17 00:00:00 2001 From: hqm19 Date: Tue, 2 Apr 2024 18:23:57 +0800 Subject: [PATCH 1/3] add promise support to custom_node_render Signed-off-by: hqm19 --- src/jsmind.common.js | 19 +++++++ src/jsmind.js | 40 +++++++++------ src/jsmind.view_provider.js | 99 ++++++++++++++++++++++++++++--------- 3 files changed, 120 insertions(+), 38 deletions(-) diff --git a/src/jsmind.common.js b/src/jsmind.common.js index cefa336e..ee9b4fb4 100644 --- a/src/jsmind.common.js +++ b/src/jsmind.common.js @@ -84,3 +84,22 @@ function setup_logger_level(log_level) { logger.error = console.error; } } + +// 如果 promise 不为空,则认为是 promise,执行 promise.then(follow_logic) 并返回新的 promise +export function then_if_promise(promise, follow_logic) { + if (!!promise) { + return promise.then(() => { + follow_logic(); + }); + } else { + follow_logic(); + } +} + +export function is_promise(obj) { + return ( + !!obj && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function' + ); +} diff --git a/src/jsmind.js b/src/jsmind.js index 8ec1a0bb..29116d50 100644 --- a/src/jsmind.js +++ b/src/jsmind.js @@ -7,6 +7,7 @@ */ import { __version__, logger, EventType, Direction, LogLevel } from './jsmind.common.js'; +import { then_if_promise } from './jsmind.common.js'; import { merge_option } from './jsmind.option.js'; import { Mind } from './jsmind.mind.js'; import { Node } from './jsmind.node.js'; @@ -285,16 +286,21 @@ export default class jsMind { logger.debug('data.load ok'); } - this.view.load(); + const view_load_promise = this.view.load(); logger.debug('view.load ok'); - this.layout.layout(); - logger.debug('layout.layout ok'); + const tail_process = () => { + this.layout.layout(); + logger.debug('layout.layout ok'); + + this.view.show(true); + logger.debug('view.show ok'); - this.view.show(true); - logger.debug('view.show ok'); + this.invoke_event_handle(EventType.show, { data: [mind] }); + //logger.info('[jsmind._show] tail_process done.'); + }; - this.invoke_event_handle(EventType.show, { data: [mind] }); + then_if_promise(view_load_promise, tail_process); } show(mind) { this._reset(); @@ -430,6 +436,7 @@ export default class jsMind { } } update_node(node_id, topic) { + //logger.info('[jsmind.update_node] node_id:' + node_id + ', topic:', topic); if (this.get_editable()) { if (_util.text.is_empty(topic)) { logger.warn('fail, topic can not be empty'); @@ -443,14 +450,19 @@ export default class jsMind { return; } node.topic = topic; - this.view.update_node(node); - this.layout.layout(); - this.view.show(false); - this.invoke_event_handle(EventType.edit, { - evt: 'update_node', - data: [node_id, topic], - node: node_id, - }); + const promise = this.view.update_node(node); + const follow_logic = () => { + this.layout.layout(); + this.view.show(false); + this.invoke_event_handle(EventType.edit, { + evt: 'update_node', + data: [node_id, topic], + node: node_id, + }); + //logger.info('[jsmind.update_node] follow_logic node_id:' + node_id); + }; + //logger.info('[jsmind.update_node] view.update_node return promise?:', !!promise); + return then_if_promise(promise, follow_logic); } } else { logger.error('fail, this mind map is not editable'); diff --git a/src/jsmind.view_provider.js b/src/jsmind.view_provider.js index 542dae60..1a7a8f77 100644 --- a/src/jsmind.view_provider.js +++ b/src/jsmind.view_provider.js @@ -5,7 +5,7 @@ * Project Home: * https://github.com/hizzgdev/jsmind/ */ -import { logger, EventType } from './jsmind.common.js'; +import { logger, EventType, then_if_promise, is_promise } from './jsmind.common.js'; import { $ } from './jsmind.dom.js'; import { init_graph } from './jsmind.graph.js'; import { util } from './jsmind.util.js'; @@ -137,10 +137,10 @@ export class ViewProvider { } } load() { - logger.debug('view.load'); this.setup_canvas_draggable(this.opts.draggable); - this.init_nodes(); + const init_res = this.init_nodes(); this._initialized = true; + return init_res; } expand_size() { var min_size = this.layout.get_min_size(); @@ -165,18 +165,38 @@ export class ViewProvider { init_nodes() { var nodes = this.jm.mind.nodes; var doc_frag = $.d.createDocumentFragment(); - for (var nodeid in nodes) { - this.create_node_element(nodes[nodeid], doc_frag); + const no_promise_ids = []; + const promises = []; + + for (let nodeid in nodes) { + const obj = this.create_node_element(nodes[nodeid], doc_frag); + if (is_promise(obj)) { + // 如果返回是一个 promise,等待 promise 结束后再异步执行后续逻辑 + promises.push( + obj.then(() => { + //logger.info('[view.init_nodes] promise then. node:', nodes[nodeid]); + this.run_in_c11y_mode_if_needed(() => { + this.init_nodes_size(nodes[nodeid]); + }); + }) + ); + } else { + no_promise_ids.push(nodeid); + } } this.e_nodes.appendChild(doc_frag); this.run_in_c11y_mode_if_needed(() => { - for (var nodeid in nodes) { + for (var nodeid of no_promise_ids) { this.init_nodes_size(nodes[nodeid]); } }); + if (promises.length > 0) { + return Promise.all(promises); //该 Promise 在所有子 Promise 完成后解决 + } } add_node(node) { + //logger.info('[view.add_node] node:', node); this.create_node_element(node, this.e_nodes); this.run_in_c11y_mode_if_needed(() => { this.init_nodes_size(node); @@ -218,8 +238,13 @@ export class ViewProvider { parent_node.appendChild(d_e); view_data.expander = d_e; } + let render_promise; if (!!node.topic) { - this.render_node(d, node); + //logger.info('[view.create_node_element] node:', node); + const obj = this.render_node(d, node); + if (is_promise(obj)) { + render_promise = obj; + } } d.setAttribute('nodeid', node.id); d.style.visibility = 'hidden'; @@ -227,6 +252,9 @@ export class ViewProvider { parent_node.appendChild(d); view_data.element = d; + if (!!render_promise) { + return render_promise; + } } remove_node(node) { if (this.selected_node != null && this.selected_node.id == node.id) { @@ -252,20 +280,30 @@ export class ViewProvider { } update_node(node) { var view_data = node._data.view; - var element = view_data.element; + let element = view_data.element; + let render_promise; if (!!node.topic) { - this.render_node(element, node); - } - if (this.layout.is_visible(node)) { - view_data.width = element.clientWidth; - view_data.height = element.clientHeight; - } else { - let origin_style = element.getAttribute('style'); - element.style = 'visibility: visible; left:0; top:0;'; - view_data.width = element.clientWidth; - view_data.height = element.clientHeight; - element.style = origin_style; + //logger.info('[view.update_node] node:', node); + console.trace('update_node'); + const obj = this.render_node(element, node); + if (is_promise(obj)) { + //logger.info('[view.update_node] render_node returns promise. node:', node); + render_promise = obj; + } } + const follow_logic = () => { + if (this.layout.is_visible(node)) { + view_data.width = element.clientWidth; + view_data.height = element.clientHeight; + } else { + let origin_style = element.getAttribute('style'); + element.style = 'visibility: visible; left:0; top:0;'; + view_data.width = element.clientWidth; + view_data.height = element.clientHeight; + element.style = origin_style; + } + }; + return then_if_promise(render_promise, follow_logic); } select_node(node) { if (!!this.selected_node) { @@ -314,6 +352,7 @@ export class ViewProvider { this.e_editor.select(); } edit_node_end() { + let render_promise; if (this.editing_node != null) { var node = this.editing_node; this.editing_node = null; @@ -322,13 +361,22 @@ export class ViewProvider { var topic = this.e_editor.value; element.style.zIndex = 'auto'; element.removeChild(this.e_editor); + + //logger.info('[view.edit_node_end] node:', node); + let obj; if (util.text.is_empty(topic) || node.topic === topic) { - this.render_node(element, node); + obj = this.render_node(element, node); } else { - this.jm.update_node(node.id, topic); + obj = this.jm.update_node(node.id, topic); + } + if (is_promise(obj)) { + render_promise = obj; } } - this.e_panel.focus(); + const _this = this; + then_if_promise(render_promise, () => { + _this.e_panel.focus(); + }); } get_view_offset() { var bounds = this.layout.bounds; @@ -494,8 +542,11 @@ export class ViewProvider { } } _custom_node_render(ele, node) { - let rendered = this.opts.custom_node_render(this.jm, ele, node); - if (!rendered) { + let obj = this.opts.custom_node_render(this.jm, ele, node); + if (is_promise(obj)) { + return obj; + } + if (!obj) { this._default_node_render(ele, node); } } From 7e9cfc1fc7ee3abc3e5cd44fefbf9387ecf25423 Mon Sep 17 00:00:00 2001 From: hqm19 Date: Tue, 2 Apr 2024 22:03:57 +0800 Subject: [PATCH 2/3] resolve react rerender problem by creating a new node when updating Signed-off-by: hqm19 --- src/jsmind.js | 11 ++++----- src/jsmind.view_provider.js | 45 +++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/jsmind.js b/src/jsmind.js index 29116d50..247f6214 100644 --- a/src/jsmind.js +++ b/src/jsmind.js @@ -444,11 +444,12 @@ export default class jsMind { } var node = this.get_node(node_id); if (!!node) { - if (node.topic === topic) { - logger.info('nothing changed'); - this.view.update_node(node); - return; - } + // 为了在 react 场景下,能通过编辑不修改退出这个场景,暂时注释掉下面代码 + // if (node.topic === topic) { + // logger.info('nothing changed'); + // this.view.update_node(node); + // return; + // } node.topic = topic; const promise = this.view.update_node(node); const follow_logic = () => { diff --git a/src/jsmind.view_provider.js b/src/jsmind.view_provider.js index 1a7a8f77..aeac5c47 100644 --- a/src/jsmind.view_provider.js +++ b/src/jsmind.view_provider.js @@ -284,11 +284,30 @@ export class ViewProvider { let render_promise; if (!!node.topic) { //logger.info('[view.update_node] node:', node); - console.trace('update_node'); - const obj = this.render_node(element, node); + + //const obj = this.render_node(element, node); + // 这里如果直接复用 element 来渲染, react 会报错: Warning: render(...): It looks like the React-rendered + // content of this container was removed without using React. This is not supported and will cause errors. + // Instead, call ReactDOM.unmountComponentAtNode to empty a container. + // const obj = this.render_node(element, node); + // 所以新建一个节点来渲染,并替换掉原来的节点 + let d = element; + if (view_data.promise_rendered) { + d = $.c('jmnode'); + } + const obj = this.render_node(d, node); if (is_promise(obj)) { //logger.info('[view.update_node] render_node returns promise. node:', node); - render_promise = obj; + render_promise = obj.then(() => { + if (!!element.parentNode) { + element.parentNode.replaceChild(d, element); + } + view_data.element = d; + element = d; + d.setAttribute('nodeid', node.id); + d.style.visibility = 'hidden'; + this._reset_node_custom_style(d, node.data); + }); } } const follow_logic = () => { @@ -365,7 +384,24 @@ export class ViewProvider { //logger.info('[view.edit_node_end] node:', node); let obj; if (util.text.is_empty(topic) || node.topic === topic) { - obj = this.render_node(element, node); + // //obj = this.render_node(element, node); + // //obj = this.update_node(node); + // let d = element; + // if (view_data.promise_rendered) { + // d = $.c('jmnode'); + // } + // obj = this.render_node(d, node); + // if (is_promise(obj)) { + // obj = obj.then(() => { + // element.parentNode.replaceChild(d, element); + // view_data.element = d; + // element = d; + // d.setAttribute('nodeid', node.id); + // this._reset_node_custom_style(d, node.data); + // }); + // } + // 上面代码,总是在编辑不改变内容退出时,节点消失。所以暂时和下面代码保持一致 + obj = this.jm.update_node(node.id, topic); } else { obj = this.jm.update_node(node.id, topic); } @@ -544,6 +580,7 @@ export class ViewProvider { _custom_node_render(ele, node) { let obj = this.opts.custom_node_render(this.jm, ele, node); if (is_promise(obj)) { + node._data.view.promise_rendered = true; return obj; } if (!obj) { From ead95040299ca39886f99a84532185e20b3dbabd Mon Sep 17 00:00:00 2001 From: hqm19 Date: Thu, 4 Apr 2024 14:28:37 +0800 Subject: [PATCH 3/3] refact add_node using promise & return promise only from render_node tail end Signed-off-by: hqm19 --- src/jsmind.js | 53 ++++++++------- src/jsmind.shortcut_provider.js | 11 ++-- src/jsmind.view_provider.js | 113 ++++++++++---------------------- 3 files changed, 67 insertions(+), 110 deletions(-) diff --git a/src/jsmind.js b/src/jsmind.js index 247f6214..c6eec5c9 100644 --- a/src/jsmind.js +++ b/src/jsmind.js @@ -335,22 +335,23 @@ export default class jsMind { } var node = this.mind.add_node(the_parent_node, node_id, topic, data, dir); if (!!node) { - this.view.add_node(node); - this.layout.layout(); - this.view.show(false); - this.view.reset_node_custom_style(node); - this.expand_node(the_parent_node); - this.invoke_event_handle(EventType.edit, { - evt: 'add_node', - data: [the_parent_node.id, node_id, topic, data, dir], - node: node_id, + return this.view.add_node(node).then(() => { + this.layout.layout(); + this.view.show(false); + this.view.reset_node_custom_style(node); + this.expand_node(the_parent_node); + this.invoke_event_handle(EventType.edit, { + evt: 'add_node', + data: [the_parent_node.id, node_id, topic, data, dir], + node: node_id, + }); + return node; }); } - return node; } else { logger.error('fail, this mind map is not editable'); - return null; } + return Promise.resolve(null); } insert_node_before(node_before, node_id, topic, data, direction) { if (this.get_editable()) { @@ -361,13 +362,14 @@ export default class jsMind { } var node = this.mind.insert_node_before(the_node_before, node_id, topic, data, dir); if (!!node) { - this.view.add_node(node); - this.layout.layout(); - this.view.show(false); - this.invoke_event_handle(EventType.edit, { - evt: 'insert_node_before', - data: [the_node_before.id, node_id, topic, data, dir], - node: node_id, + this.view.add_node(node).then(() => { + this.layout.layout(); + this.view.show(false); + this.invoke_event_handle(EventType.edit, { + evt: 'insert_node_before', + data: [the_node_before.id, node_id, topic, data, dir], + node: node_id, + }); }); } return node; @@ -385,13 +387,14 @@ export default class jsMind { } var node = this.mind.insert_node_after(the_node_after, node_id, topic, data, dir); if (!!node) { - this.view.add_node(node); - this.layout.layout(); - this.view.show(false); - this.invoke_event_handle(EventType.edit, { - evt: 'insert_node_after', - data: [the_node_after.id, node_id, topic, data, dir], - node: node_id, + this.view.add_node(node).then(() => { + this.layout.layout(); + this.view.show(false); + this.invoke_event_handle(EventType.edit, { + evt: 'insert_node_after', + data: [the_node_after.id, node_id, topic, data, dir], + node: node_id, + }); }); } return node; diff --git a/src/jsmind.shortcut_provider.js b/src/jsmind.shortcut_provider.js index 38983ead..d3c0fb7c 100644 --- a/src/jsmind.shortcut_provider.js +++ b/src/jsmind.shortcut_provider.js @@ -81,11 +81,12 @@ export class ShortcutProvider { var selected_node = _jm.get_selected_node(); if (!!selected_node) { var node_id = this._newid(); - var node = _jm.add_node(selected_node, node_id, 'New Node'); - if (!!node) { - _jm.select_node(node_id); - _jm.begin_edit(node_id); - } + _jm.add_node(selected_node, node_id, 'New Node').then(node => { + if (!!node) { + _jm.select_node(node_id); + _jm.begin_edit(node_id); + } + }); } } handle_addbrother(_jm, e) { diff --git a/src/jsmind.view_provider.js b/src/jsmind.view_provider.js index aeac5c47..d6f0d60a 100644 --- a/src/jsmind.view_provider.js +++ b/src/jsmind.view_provider.js @@ -165,41 +165,26 @@ export class ViewProvider { init_nodes() { var nodes = this.jm.mind.nodes; var doc_frag = $.d.createDocumentFragment(); - const no_promise_ids = []; const promises = []; for (let nodeid in nodes) { - const obj = this.create_node_element(nodes[nodeid], doc_frag); - if (is_promise(obj)) { - // 如果返回是一个 promise,等待 promise 结束后再异步执行后续逻辑 - promises.push( - obj.then(() => { - //logger.info('[view.init_nodes] promise then. node:', nodes[nodeid]); - this.run_in_c11y_mode_if_needed(() => { - this.init_nodes_size(nodes[nodeid]); - }); - }) - ); - } else { - no_promise_ids.push(nodeid); - } + promises.push( + this.create_node_element(nodes[nodeid], doc_frag).then(() => { + this.run_in_c11y_mode_if_needed(() => { + this.init_nodes_size(nodes[nodeid]); + }); + }) + ); } this.e_nodes.appendChild(doc_frag); - - this.run_in_c11y_mode_if_needed(() => { - for (var nodeid of no_promise_ids) { - this.init_nodes_size(nodes[nodeid]); - } - }); - if (promises.length > 0) { - return Promise.all(promises); //该 Promise 在所有子 Promise 完成后解决 - } + return Promise.all(promises); //该 Promise 在所有子 Promise 完成后解决 } add_node(node) { //logger.info('[view.add_node] node:', node); - this.create_node_element(node, this.e_nodes); - this.run_in_c11y_mode_if_needed(() => { - this.init_nodes_size(node); + return this.create_node_element(node, this.e_nodes).then(() => { + this.run_in_c11y_mode_if_needed(() => { + this.init_nodes_size(node); + }); }); } run_in_c11y_mode_if_needed(func) { @@ -238,13 +223,9 @@ export class ViewProvider { parent_node.appendChild(d_e); view_data.expander = d_e; } - let render_promise; + let render_promise = Promise.resolve(); if (!!node.topic) { - //logger.info('[view.create_node_element] node:', node); - const obj = this.render_node(d, node); - if (is_promise(obj)) { - render_promise = obj; - } + render_promise = this.render_node(d, node); } d.setAttribute('nodeid', node.id); d.style.visibility = 'hidden'; @@ -252,9 +233,7 @@ export class ViewProvider { parent_node.appendChild(d); view_data.element = d; - if (!!render_promise) { - return render_promise; - } + return render_promise; } remove_node(node) { if (this.selected_node != null && this.selected_node.id == node.id) { @@ -281,10 +260,8 @@ export class ViewProvider { update_node(node) { var view_data = node._data.view; let element = view_data.element; - let render_promise; + let render_promise = Promise.resolve(); if (!!node.topic) { - //logger.info('[view.update_node] node:', node); - //const obj = this.render_node(element, node); // 这里如果直接复用 element 来渲染, react 会报错: Warning: render(...): It looks like the React-rendered // content of this container was removed without using React. This is not supported and will cause errors. @@ -295,22 +272,19 @@ export class ViewProvider { if (view_data.promise_rendered) { d = $.c('jmnode'); } - const obj = this.render_node(d, node); - if (is_promise(obj)) { - //logger.info('[view.update_node] render_node returns promise. node:', node); - render_promise = obj.then(() => { - if (!!element.parentNode) { - element.parentNode.replaceChild(d, element); - } - view_data.element = d; - element = d; - d.setAttribute('nodeid', node.id); - d.style.visibility = 'hidden'; - this._reset_node_custom_style(d, node.data); - }); - } + + render_promise = this.render_node(d, node).then(() => { + if (!!element.parentNode) { + element.parentNode.replaceChild(d, element); + } + view_data.element = d; + element = d; + d.setAttribute('nodeid', node.id); + d.style.visibility = 'hidden'; + this._reset_node_custom_style(d, node.data); + }); } - const follow_logic = () => { + return render_promise.then(() => { if (this.layout.is_visible(node)) { view_data.width = element.clientWidth; view_data.height = element.clientHeight; @@ -321,8 +295,7 @@ export class ViewProvider { view_data.height = element.clientHeight; element.style = origin_style; } - }; - return then_if_promise(render_promise, follow_logic); + }); } select_node(node) { if (!!this.selected_node) { @@ -382,29 +355,7 @@ export class ViewProvider { element.removeChild(this.e_editor); //logger.info('[view.edit_node_end] node:', node); - let obj; - if (util.text.is_empty(topic) || node.topic === topic) { - // //obj = this.render_node(element, node); - // //obj = this.update_node(node); - // let d = element; - // if (view_data.promise_rendered) { - // d = $.c('jmnode'); - // } - // obj = this.render_node(d, node); - // if (is_promise(obj)) { - // obj = obj.then(() => { - // element.parentNode.replaceChild(d, element); - // view_data.element = d; - // element = d; - // d.setAttribute('nodeid', node.id); - // this._reset_node_custom_style(d, node.data); - // }); - // } - // 上面代码,总是在编辑不改变内容退出时,节点消失。所以暂时和下面代码保持一致 - obj = this.jm.update_node(node.id, topic); - } else { - obj = this.jm.update_node(node.id, topic); - } + let obj = this.jm.update_node(node.id, topic); if (is_promise(obj)) { render_promise = obj; } @@ -576,6 +527,7 @@ export class ViewProvider { } else { $.t(ele, node.topic); } + return Promise.resolve(); } _custom_node_render(ele, node) { let obj = this.opts.custom_node_render(this.jm, ele, node); @@ -584,8 +536,9 @@ export class ViewProvider { return obj; } if (!obj) { - this._default_node_render(ele, node); + return this._default_node_render(ele, node); } + return Promise.resolve(); } reset_node_custom_style(node) { this._reset_node_custom_style(node._data.view.element, node.data);