vercengen/components/ComponentUndoRedo.js

/**
 * Refer to <span color = "yellow">{@link ve.Component}</span> for methods or fields inherited from this Component's parent such as `.options.attributes` or `.element`.
 * 
 * Undo/Redo interface with the ability for users to navigate between different {@link DALS.Timeline} instances. Undo/Redo tree by default with both a canvas interface and HTML list.
 * 
 * ##### Constructor:
 * - `arg0_value=DALS.Timeline.current_timeline`: {@link string} - The timeline ID the initial value should be set to.
 * - `arg1_options`: {@link Object}
 * 
 * ##### Instance:
 * - `.v`: {@link string}
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.UndoRedo.draw|draw}</span>() - Redraws both HTML/canvas-side elements.
 * - <span color=00ffff>{@link ve.UndoRedo.handleEvents|handleEvents}</span>() - Handles events for zooming/panning around canvas.
 * - <span color=00ffff>{@link ve.UndoRedo.loadSettings|loadSettings}</span>() - Loads settings from registry file.
 * - <span color-00ffff>{@link ve.UndoRedo.saveSettings|saveSettings}</span>() - Saves settings to registry file.
 * 
 * @augments ve.Component
 * @memberof ve.Component
 * @type {ve.UndoRedo}
 */
ve.UndoRedo = class extends ve.Component {
	constructor (arg0_value, arg1_options) {
		let value = arg0_value;
		let options = {
			sort_mode: "last_modified",
			...arg1_options
		};
			super(options);
			
		//Initialise options
		if (options.flipped !== false) options.flipped = true;
		
		//Declare local instance variables
		this.element = document.createElement("div");
			this.element.setAttribute("component", "ve-undo-redo");
			this.element.instance = this;
			if (options.attributes)
				Object.iterate(options.attributes, (local_key, local_value) => this.element.setAttribute(local_key, local_value.toString()));
			
		this.options = options;
		this.value = value; //Stores timeline_id
		
		//Add HTML list, canvas
		this.canvas_container_el = document.createElement("div");
			this.canvas_el = document.createElement("canvas");
			this.canvas_container_el.id = `canvas-container`;
			this.canvas_container_el.appendChild(this.canvas_el);
		this.html_list_el = document.createElement("div");
		this.loadSettings(); //Load settings upon initialisation
		
		//Create a ve.PageMenu with this.html_list_el, this.canvas_container_el, and mount it to this.element
		let settings_obj = ve.registry.settings.UndoRedo;
		
		let actions_bar = () => new ve.RawInterface({
			row_one: new ve.RawInterface({
				commit_name: new ve.Text(settings_obj.manual_commit_name, {
					name: "Commit Name",
					onuserchange: (v) => {
						settings_obj.manual_commit_name = v;
						this.saveSettings();
					}
				}),
				save_commit: new ve.Button(() => {
					try {
						let state_obj = DALS.Timeline.saveState();
						
						//Add new DALS Action under load_save
						new DALS.Action({
							options: { 
								name: `Save Commit ${(settings_obj.manual_commit_name?.length > 0) ? `(${settings_obj.manual_commit_name})` : ""}<br>${String.formatDate()}`, 
								key: `save_commit_${Date.now()}` 
							},
							value: {
								type: "global",
								load_save: state_obj
							}
						});
					} catch (e) { console.warn(e); }
				}, { 
					name: "Save Commit",
					style: { marginLeft: `var(--padding)` }
				})
			}, {
				limit: () => (settings_obj.manual_commits)
			}),
			row_two: new ve.RawInterface({
				undo_button: new ve.Button(() => DALS.Timeline.undo(), {
					name: "<icon>undo</icon>", tooltip: "Undo" }),
				redo_button: new ve.Button(() => DALS.Timeline.redo(), {
					name: "<icon>redo</icon>", tooltip: "Redo" }),
				manual_commits: new ve.Toggle(settings_obj.manual_commits, {
					name: "Manual Commits",
					onuserchange: (v) => {
						settings_obj.manual_commits = v;
						this.saveSettings();
					},
					style: {
						marginLeft: `var(--padding)`
					},
					tooltip: "Whether to enable manual commits."
				})
			})
		}, { 
			name: " ",
			style: {
				'[component="ve-raw-interface"]': {
					alignItems: "anchor-center",
					display: "flex",
					marginBottom: "var(--padding)"
				}
			}
		});
		
		this.page_menu = new ve.PageMenu({
			current_timeline: {
				name: loc("ve.registry.localisation.UndoRedo_timeline_view"),
				components_obj: {
					actions_bar: actions_bar(),
					html: new ve.HTML(this.html_list_el)
				}
			},
			timeline_map: {
				name: loc("ve.registry.localisation.UndoRedo_actions_map"),
				components_obj: {
					actions_bar: actions_bar(),
					html: new ve.HTML(this.canvas_container_el),
					coords_display: new ve.HTML(() => 
						loc("ve.registry.localisation.UndoRedo_coords_display", 
							String.formatNumber(this.translate_x, 2), 
							String.formatNumber(this.translate_y, 2), 
							String.formatNumber(this.scale, 2))
					)
				}
			}
		}, { 
			name: this.options.name,
			onchange: (v, e) => {
				//Reinstantiate actions_bar
				try { this.page_menu.interfaces_obj[v].actions_bar.v = actions_bar().v; } catch (e) {}
				
				if (v === "current_timeline") {
					this.from_binding_fire_silently = true;
					this.v = DALS.Timeline.current_timeline;
					delete this.from_binding_fire_silently;
				}
			}
		});
		this.element.appendChild(this.page_menu.element);
		
		//KEEP AT BOTTOM!
		setTimeout(() => {
			this.draw();
			this.handleEvents();
		});
		this.v = DALS.Timeline.current_timeline;
	}
	
	/**
	 * Returns the current {@link DALS.Timeline} object of the present Component.
	 * - Accessor of: {@link ve.UndoRedo}
	 *
	 * @alias v
	 * @memberof ve.Component.ve.UndoRedo
	 * @type {DALS.Timeline}
	 */
	get v () {
		//Return statement
		return DALS.Timeline.getTimeline(this.value);
	}
	
	/**
	 * Sets the new {@link DALS.Timeline} from a timeline ID.
	 * - Method of: {@link ve.UndoRedo}
	 *
	 * @alias v
	 * @memberof ve.Component.ve.UndoRedo
	 * 
	 * @param arg0_value
	 */
	set v (arg0_value) {
		//Convert from parameters
		let value = DALS.Timeline.current_timeline;
			if (arg0_value instanceof DALS.Timeline) value = arg0_value.id;
			if (typeof arg0_value === "string") value = arg0_value;
			if (arg0_value === undefined) value = DALS.Timeline.current_timeline;
		
		//Declare local instance variables
		this.value = value;
		
		//Render Canvas list; scavenge code from old Naissance
		{
			this.draw(true);
			this.fireFromBinding();
		}
	}
	
	/**
	 * Redraws the current interface.
	 * - Method of: {@link ve.UndoRedo}
	 *
	 * @alias draw
	 * @memberof ve.Component.ve.UndoRedo
	 */
	draw (arg0_force_update) {
		//Convert from parameters
		let force_update = arg0_force_update;
		
		//Force update if DALS.Timeline.current_timeline has changed
		if (
			DALS.Timeline.current_timeline !== this.current_timeline ||
			DALS.Timeline.current_index !== this.current_index
		) {
			force_update = true;
			this.value = DALS.Timeline.current_timeline;
		}
		
		//Render canvas
		{
			//Declare local instance variables
			let ctx = this.canvas_el.getContext("2d");
			let timeline_graph;
			if (!this.options.flipped) {
				timeline_graph = DALS.Timeline.generateGraph();
			} else {
				for (let i = 0; i < DALS.Timeline.instances.length; i++)
					if (DALS.Timeline.instances[i].initial_timeline) {
						timeline_graph = DALS.Timeline.instances[i].generateFlippedGraph();
						break;
					}
			}
			
			let canvas_height = 0;
			let canvas_width = 0;
			let node_height = 14;
			let spacing_x = 240;
			let spacing_y = 60;
			
			//Store node positions for event handling
			let node_positions = {};
			let row_tracker = {};
			
			//Clear previous render
			ctx.clearRect(0, 0, this.canvas_el.width, this.canvas_el.height);
			
			//1. Iterate over timeline_graph keys and render nodes
			this.unique_timeline_ids = (this.unique_timeline_ids) ? this.unique_timeline_ids : [];
			
			Object.iterate(timeline_graph, (local_key, local_value) => {
				let local_x = local_value.x*spacing_x - 50;
				let local_y = local_value.y*spacing_y + 10;
				
				//Initialise row_tracker
				if (!row_tracker[local_value.y]) row_tracker[local_value.y] = [];
				row_tracker[local_value.y].push(local_key);
				
				//Measure text width and define node height
				let is_selected = false;
				if (
					local_value.timeline_id === DALS.Timeline.current_timeline && 
					DALS.Timeline.current_index >= local_value.value.options.domain[0] + 1 &&
					DALS.Timeline.current_index <= local_value.value.options.domain[1] + 1
				)
					is_selected = true;
				
				let node_text;
				if (local_value.value.options && local_value.value.options.name)
					node_text = `${local_value.value.options.name} (${String.formatNumber(local_value.value.options.length)})`;
				if (node_text === undefined) node_text = loc("ve.registry.localisation.UndoRedo_unlisted");
				if (local_value.child_timelines && local_value.x === 1)
					node_text = loc("ve.registry.localisation.UndoRedo_initialisation");
				if (local_value.parent_timeline)
					node_text = loc("ve.registry.localisation.UndoRedo_split_from_timeline");
				let text_height = node_text.split("\n").length*node_height;
				let text_width = ctx.measureText(node_text).width;
				
				//Store position for click detection
				//console.log(local_value, local_value.timeline_index);
				node_positions[local_key] = {
					id: `${local_value.x}-${local_value.y}`,
					name: node_text,
					
					is_selected: is_selected,
					timeline_id: local_value.timeline_id,
					timeline_index: local_value.timeline_index,
					value: local_value.value,
					is_branch_point: (local_value.timeline_group_index === 0),
					
					height: text_height,
					width: text_width,
					x: local_x,
					y: local_y
				};
			});
			
			//2. Calculate canvas.height, canvas.width
			Object.iterate(node_positions, (local_key, local_value) => {
				canvas_height = Math.max(canvas_height, Math.returnSafeNumber(local_value.y + local_value.height));
				canvas_width = Math.max(canvas_width, Math.returnSafeNumber(local_value.x + local_value.width));
			});
			this.canvas_el.setAttribute("height", canvas_height);
			this.canvas_el.setAttribute("width", canvas_width);
			
			//3. Draw DALS.Action nodes
			Object.iterate(node_positions, (local_key, local_value) => {
				if (local_value.is_selected) {
					ctx.fillStyle = `rgb(235, 235, 235)`;
					ctx.fillRect(local_value.x - local_value.width/2 - local_value.height/2, local_value.y - local_value.height, local_value.width + local_value.height, local_value.height*2);
				}
				ctx.fillStyle = (!local_value.is_selected) ? "white" : "black";
				ctx.font = `${node_height}px Karla Light`;
				ctx.textAlign = "center";
				ctx.textBaseline = "middle";
				ctx.fillText(local_value.name, local_value.x, local_value.y);
			});
			
			//4. Draw horizontal lines
			Object.iterate(row_tracker, (local_key, local_value) => {
				local_value = local_value.sort((a, b) => node_positions[a].x - node_positions[b].x);
				
				for (let i = 0; i < local_value.length - 1; i++) {
					let local_start_key = local_value[i];
					let local_end_key = local_value[i + 1];
					
					let local_start_node = node_positions[local_start_key];
					let local_end_node = node_positions[local_end_key];
					
					//Only draw if end node is a branch point
					if (local_end_node.is_branch_point) {
						let local_start_x = local_start_node.x + local_start_node.width/2 + local_start_node.height/2;
						let local_start_y = local_start_node.y;
						let local_end_x = local_end_node.x - local_end_node.width/2 - local_end_node.height/2;
						let local_end_y = local_end_node.y;
						
						ctx.beginPath();
						ctx.moveTo(local_start_x, local_start_y);
						ctx.lineTo(local_end_x, local_end_y);
						ctx.strokeStyle = "white";
						ctx.lineWidth = 2;
						ctx.stroke();
						ctx.closePath();
					}
				}
			});
			
			//5. Draw vertical lines
			Object.iterate(timeline_graph, (local_key, local_value) => {
				let local_node = node_positions[local_key];
				let local_start_x = local_node.x;
				let local_start_y = local_node.y - local_node.height;
				
				if (local_value.connection_ids)
					for (let i = 0; i < local_value.connection_ids.length; i++)
						if (node_positions[local_value.connection_ids[i]]) {
							let local_connecting_node = node_positions[local_value.connection_ids[i]];
							let local_end_x = local_connecting_node.x;
							let local_end_y = local_connecting_node.y + local_node.height;
							
							//Draw line between nodes
							ctx.beginPath();
							ctx.moveTo(local_start_x, local_start_y);
							ctx.lineTo(local_end_x, local_end_y);
							ctx.strokeStyle = "white";
							ctx.lineWidth = 2;
							ctx.stroke();
							ctx.closePath();
						}
			});
			
			//6. Add click event listener to detect node clicks
			this.canvas_el.onclick = (e) => {
				let canvas_el_rect = this.canvas_el.getBoundingClientRect();
				let click_x = (e.clientX - canvas_el_rect.left)/this.scale;
				let click_y = (e.clientY - canvas_el_rect.top)/this.scale;
				
				//Iterate over all node_positions
				Object.iterate(node_positions, (local_key, local_value) => {
					if (
						click_x >= local_value.x - local_value.width/2 && click_x <= local_value.x + local_value.width/2 && click_y >= local_value.y - local_value.height/2 && click_y <= local_value.y + local_value.height/2
					) {
						let local_timeline = DALS.Timeline.getTimeline(local_value.timeline_id);
						console.log(`Calling local_timeline.jumpToAction(${Math.returnSafeNumber(local_value.value.options.domain[1]) + 1})!`);
						local_timeline.jumpToAction(Math.returnSafeNumber(local_value.value.options.domain[1]) + 1);
						
						if (local_timeline.id !== DALS.Timeline.current_timeline) {
							this.from_binding_fire_silently = true;
								DALS.Timeline.current_timeline = local_timeline.id;
								this.v = local_timeline.id;
								this.fireToBinding();
							delete this.from_binding_fire_silently;
						}
					}
				});
			};
		}
		
		//1. Render HTML
		{
			//Declare local instance variables
			let skip_html_redraw = false;
			let skip_timeline_redraw = false;
			let timeline_obj = DALS.Timeline.getTimeline(this.value);
			
			if (this.html_timeline_length === timeline_obj.value.length) skip_html_redraw = true;
			if (this.timeline_select_obj)
				if (DALS.Timeline.instances.length === Object.keys(this.timeline_select_obj).length) skip_timeline_redraw = true;
				
			if (!(skip_html_redraw && skip_timeline_redraw) || force_update) {
				this.html_timeline_length = timeline_obj.value.length;
				
				if (!timeline_obj) {
					this.value = DALS.Timeline.current_timeline;
					timeline_obj = DALS.Timeline.getTimeline(this.value);
				}
				
				this.html_list_el.innerHTML = "";
				this.timeline_select_obj = {};
				
				//Iterate over all DALS.Timeline.instances and list them in order of length
				if (this.options.sort_mode === "alphabetical_ascending") {
					DALS.Timeline.instances.sort((a, b) => a.name.localeCompare(b.name));
				} else if (this.options.sort_mode === "alphabetical_descending") {
					DALS.Timeline.instances.sort((a, b) => a.name.localeCompare(b.name));
					DALS.Timeline.instances.reverse();
				} else if (this.options.sort_mode === "chronological_ascending") {
					DALS.Timeline.instances.sort((a, b) => a.date_created.getTime() - b.date_created.getTime());
				} else if (this.options.sort_mode === "chronological_descending") {
					DALS.Timeline.instances.sort((a, b) => b.date_created.getTime() - a.date_created.getTime());
				} else if (this.options.sort_mode === "last_modified") {
					DALS.Timeline.instances.sort((a, b) => b.last_modified.getTime() - a.last_modified.getTime());
				} else if (this.options.sort_mode === "least_actions") {
					DALS.Timeline.instances.sort((a, b) => a.value.length - b.value.length);
				} else if (this.options.sort_mode === "most_actions") {
					DALS.Timeline.instances.sort((a, b) => b.value.length - a.value.length);
				}
				
				for (let i = 0; i < DALS.Timeline.instances.length; i++) {
					let local_timeline = DALS.Timeline.instances[i];
					
					this.timeline_select_obj[local_timeline.id] = {
						name: `${(local_timeline.name) ? local_timeline.name : local_timeline.id} (${String.formatNumber(local_timeline.value.length)})`,
						selected: (local_timeline.id === this.value)
					};
				}
				
				this.html_select = new ve.Interface({
					select_field: new ve.Select(this.timeline_select_obj, {
						name: "Viewing:",
						onchange: (v, e) => {
							this.value = v;
							this.draw(true);
							this.fireToBinding();
						},
						x: 0, y: 0
					}),
					filter_button: new ve.Button(() => {
						let local_context_menu = new ve.ContextMenu({
							radio_select: new ve.Radio({
								alphabetical_ascending: {
									name: loc("ve.registry.localisation.UndoRedo_alphabetical_ascending")
								},
								alphabetical_descending: {
									name: loc("ve.registry.localisation.UndoRedo_alphabetical_descending")
								},
								chronological_ascending: {
									name: loc("ve.registry.localisation.UndoRedo_chronological_ascending")
								},
								chronological_descending: {
									name: loc("ve.registry.localisation.UndoRedo_chronological_descending")
								},
								last_modified: {
									name: loc("ve.registry.localisation.UndoRedo_last_modified")
								},
								least_actions: {
									name: loc("ve.registry.localisation.UndoRedo_least_actions")
								},
								most_actions: {
									name: loc("ve.registry.localisation.UndoRedo_most_actions")
								}
							}, {
								name: `<b>${loc("ve.registry.localisation.UndoRedo_sort_timelines")}</b><br><br>`,
								selected: this.options.sort_mode,
								
								onchange: (v, e) => {
									this.options.sort_mode = v;
									this.draw(true);
								}
							})
						}, { id: "ve_undo_redo_html_select_filter" });
					}, { name: "<icon>filter_alt</icon>", tooltip: loc("ve.registry.localisation.UndoRedo_sorting_filter"), x: 1, y: 0 }),
					timeline_name: new ve.Text(timeline_obj.name, { 
						name: `${loc("ve.registry.localisation.UndoRedo_name")} `,
						onchange: (v, e) => {
							timeline_obj.name = v;
							this.draw(true);
						},
						x: 0, y: 1, width: 99 
					})
				}, { is_folder: false }); 
				
				this.html_information = new ve.HTML(() => `Timeline ID: ${DALS.Timeline.current_timeline} | Index: #${DALS.Timeline.current_index}<br><li style = "margin-left: calc(var(--padding)*2);"><b>Active Branch</b>: ${DALS.Timeline.getTimeline(DALS.Timeline.current_timeline).name}</li>`);
				this.html_list_el.appendChild(this.html_select.element);
				this.html_list_el.appendChild(this.html_information.element);
				
				//Iterate over timeline_groups, and add list items depending on the length
				let current_index = 1;
				let timeline_groups = timeline_obj.getGroups();
				let ul_el = document.createElement("ul");
					ul_el.classList.add("no-markers");
				
				for (let i = 0; i < timeline_groups.length; i++) { //[WIP] - Worth checking if we need a +/-1 offset later
					//Create header_el with Jump To/Branch buttons
					let group_el = document.createElement("li");
					let local_domain =  timeline_groups[i][0].options.domain;
						local_domain = [Math.returnSafeNumber(local_domain[0]), Math.returnSafeNumber(local_domain[1])];
					let local_index = JSON.parse(JSON.stringify(current_index));
					let local_name = `New Action`;
					if (timeline_groups[i] && timeline_groups[i][0] && timeline_groups[i][0].name)
						local_name = timeline_groups[i][0].name;
					
					let is_selected = (DALS.Timeline.current_timeline === timeline_obj.id && DALS.Timeline.current_index >= local_domain[0] && DALS.Timeline.current_index <= local_domain[1]) 
						|| (local_domain[0] === 0 && DALS.Timeline.current_index === 1);
					
					let header_el = new ve.RawInterface({
						action_name: new ve.HTML(`${local_name} (${String.formatNumber(timeline_groups[i].length)})&nbsp;&nbsp;<br>#${Math.max(local_domain[0], 1)} - ${Math.max(local_domain[1], 1)}`, {
							attributes: {
								"data-is-selected": is_selected
							},
							style: {
								display: "inline-block"
							}
						}),
							
						jump_to_button: new ve.Button(() => {
							console.log(`timeline_obj.jumpToAction(${local_index} + ${timeline_groups[i].length});`, local_index + timeline_groups[i].length);
							console.log(`- local_index:`, local_index, `+ timeline_groups[i].length:`, timeline_groups[i].length);
							timeline_obj.jumpToAction(local_index + timeline_groups[i].length - 1);
							this.fireToBinding();
						}, { 
							name: `<icon>arrow_right_alt</icon>`, 
							tooltip: loc("ve.registry.localisation.UndoRedo_jump_to"),
							style: { 
								"button": {
									marginLeft: "var(--padding)"
								},
								marginLeft: "auto", order: 99 
							}
						}),
						branch: new ve.Button(() => {
							DALS.Timeline.current_index = current_index + timeline_groups[i].length;
							let new_timeline = timeline_obj.branch();
								DALS.Timeline.current_timeline = new_timeline.id;
								DALS.Timeline.current_index = 0;
								new_timeline.jumpToStart();
							this.fireToBinding();
						}, { 
							name: `<icon>alt_route</icon>`, 
							tooltip: loc("ve.registry.localisation.UndoRedo_branch_at_node"),
							style: { order: 100 }
						})
					}, {
						style: {
							alignItems: "center",
							display: "flex"
						}
					});
					
					//Append main header in div; don't split it up to prevent DOM lag
					group_el.appendChild(header_el.element);
					ul_el.appendChild(group_el);
					
					//Keep track of current_index
					current_index += timeline_groups[i].length;
				}
				
				this.html_list_el.appendChild(ul_el);
			}
		}
		
		this.current_index = JSON.parse(JSON.stringify(DALS.Timeline.current_index));
		this.current_timeline = JSON.parse(JSON.stringify(DALS.Timeline.current_timeline));
	}
	
	/**
	 * Handles events for {@link this.canvas_container_el}.
	 * - Method of: {@link ve.UndoRedo}
	 *
	 * @alias handleEvents
	 * @memberof ve.Component.ve.UndoRedo
	 */
	handleEvents () {
		//Declare local instance variables
		this.is_panning = false;
		this.scale = 1;
		this.start_x = 0;
		this.start_y = 0;
		this.translate_x = 0;
		this.translate_y = 0;
		
		//Add drag/pan options
		this.canvas_container_el.parentElement.addEventListener("mousedown", (e) => {
			if (e.button === 1) { //Middle Mouse Button (MMB)
				this.is_panning = true;
				this.start_x = e.clientX - this.translate_x;
				this.start_y = e.clientY - this.translate_y;
				e.preventDefault(); //Prevent scrolling
			}
		});
		this.canvas_container_el.parentElement.addEventListener("mouseleave", () => this.is_panning = false); //Mouseleave (stop panning)
		this.canvas_container_el.parentElement.addEventListener("mousemove", (e) => {
			if (this.is_panning) {
				this.translate_x = e.clientX - this.start_x;
				this.translate_y = e.clientY - this.start_y;
				internalHelperUndoRedoUITransform();
			}
		}); //Mousemove (only when panning)
		this.canvas_container_el.parentElement.addEventListener("mouseup", () => this.is_panning = false); //Mouseup (stop panning)
		
		//Zoom handling (scroll to zoom)
		this.canvas_container_el.parentElement.addEventListener("wheel", (e) => {
			e.preventDefault();
			let zoom_factor = 1.1;
			
			//Limit new_scale to a reasonable range
			let new_scale = (e.deltaY < 0) ? this.scale*zoom_factor : this.scale/zoom_factor;
				new_scale = Math.max(0.1, Math.min(new_scale, 5));
			let current_rect = this.canvas_container_el.getBoundingClientRect();
			let offset_x = (e.clientX - current_rect.left)/current_rect.width;
			let offset_y = (e.clientY - current_rect.top)/current_rect.height;
			
			//Adjust translation based on zoom centre
			this.scale = new_scale;
			this.translate_x -= (offset_x - 0.5)*current_rect.width*(new_scale - this.scale);
			this.translate_y -= (offset_y - 0.5)*current_rect.height*(new_scale - this.scale);
			
			internalHelperUndoRedoUITransform();
		});
		this.undo_redo_loop = setInterval(() => this.draw(), 100);
		
		let internalHelperUndoRedoUITransform = () => {
			this.canvas_container_el.style.transform = `translate(${this.translate_x}px, ${this.translate_y}px) scale(${this.scale})`;
		}
	}
	
	/**
	 * Saves settings to registry file.
	 * - Method of: {@link ve.UndoRedo}
	 *
	 * @alias loadSettings
	 * @memberof ve.Component.ve.UndoRedo
	 */
	loadSettings () {
		//Declare local instance variables
		let settings_obj = ve.registry.settings.UndoRedo;
		
		if (typeof settings_obj.save_file === "string")
			try {
				if (fs.existsSync(settings_obj.save_file))
					ve.registry.settings.UndoRedo = JSON.parse(fs.readFileSync(settings_obj.save_file, "utf8"));
			} catch (e) {}
	}
	
	/**
	 * Saves settings to registry file.
	 * - Method of: {@link ve.UndoRedo}
	 * 
	 * @alias saveSettings
	 * @memberof ve.Component.ve.UndoRedo
	 */
	saveSettings () {
		//Declare local instance variables
		let settings_obj = ve.registry.settings.UndoRedo;
		
		//Write to file
		if (typeof settings_obj.save_file === "string")
			fs.writeFileSync(settings_obj.save_file, JSON.stringify(settings_obj), "utf8");
	}
};

//Functional binding

/**
 * @returns {ve.UndoRedo}
 */
veUndoRedo = function () {
	//Return statement
	return new ve.UndoRedo(...arguments);
};