From 2cbce9a525500de904d4d67df4317e596c03471d Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 23 Jul 2020 19:52:43 -0500 Subject: [PATCH] notes: rewrite notes Javascript to object-oriented style. Rewrite the notes Javascript from a procedural style to an object-oriented style. Before the notes Javascript had a lot of problems: * There was hidden state everywhere, both locally and globally. We had state in global variables, in tags, in DOM data-* attributes (on multiple elements), and in jQuery .data() properties (which are different from data-* attributes, because they aren't visible in the DOM). * Local state was hard to reason about. There was lots of direct DOM manipulation in random places. Functions had to constantly pass around note ids and look up elements in the DOM to get the state. State was invisible because it was stored as jQuery .data() properties. It was hard to follow where state was stored, how it was initialized, and how it changed. * Global state was also a mess. There were a lot of global flags and variables only used in specific situations. Almost all of this state was unnecessary. Global state also prevented us from doing things like loading or unloading posts dynamically, or showing multiple posts with notes on the same page. * There was a lot of duplication of code, especially for placing notes, and for loading or saving new notes versus saved notes. Now the code is organized in an object-oriented fashion: * The Note class represents a single note. A post has a list of notes, and each note object has a Note.Box and a Note.Body. Together these objects encapsulate the note's state. * Notes have methods for doing things like placing note boxes, or showing and hiding note bodies, or creating, saving, or deleting notes. This makes the JS API cleaner. * Global state is kept to a minimum. This is one big patch because it was too hard to make these changes incrementally. There are a couple minor bugfixes, but the actual behavior of notes should remain unchanged. Bugfixes: * It was possible to enter translation mode, start dragging a new note, then press N to leave translation mode while still dragging the note. If you did this, then you would be stuck in translation mode and you couldn't stop dragging the note. * Placement of new notes is now pixel-perfect. Before when placing a note, the note would shift by 1-2 pixels. * Previewing an empty note didn't show the "Click to edit" message. Other noteworthy changes: * Most global state has been eliminated. There were a lot of flags and variables stored as global variables on `Danbooru.Note`. Most of these turned out to be either unnecessary or even unused. * Notes now have an explicit minimum size of 10x10 pixels. Before this limit was hardcoded and undocumented. * A lot of the note placement and note creation code has been simplified. * `Note.add()` and `Note.create()` have been refactored into `new Note()`. Before `Note.add` was used to load an existing note, while `Note.create` was used to create a new note. These did the same thing, but had slightly different behavior. * `Note.Box.scale()` and `Note.box.update_data_attributes` have been refactored into `Note.Box.place_note()`. Contrary to their names, these functions were actually both used to place notes. --- app/javascript/packs/application.js | 4 - app/javascript/src/javascripts/notes.js | 1247 ++++++++--------- app/javascript/src/javascripts/utility.js | 4 + app/javascript/src/styles/specific/notes.scss | 18 +- app/javascript/src/styles/specific/posts.scss | 8 + app/views/posts/show.html.erb | 2 +- 6 files changed, 571 insertions(+), 712 deletions(-) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 4b00f95c2..405431059 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -16,14 +16,10 @@ require("jquery-ui/ui/effects/effect-shake"); require("jquery-ui/ui/widgets/autocomplete"); require("jquery-ui/ui/widgets/button"); require("jquery-ui/ui/widgets/dialog"); -require("jquery-ui/ui/widgets/draggable"); -require("jquery-ui/ui/widgets/resizable"); require("jquery-ui/themes/base/core.css"); require("jquery-ui/themes/base/autocomplete.css"); require("jquery-ui/themes/base/button.css"); require("jquery-ui/themes/base/dialog.css"); -require("jquery-ui/themes/base/draggable.css"); -require("jquery-ui/themes/base/resizable.css"); require("jquery-ui/themes/base/theme.css"); require("@fortawesome/fontawesome-free/css/fontawesome.css"); diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index dac3d5cb3..f299a97fb 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -1,95 +1,150 @@ -import CurrentUser from './current_user' -import Utility from './utility' +import "jquery-ui/ui/widgets/draggable"; +import "jquery-ui/ui/widgets/resizable"; +import "jquery-ui/themes/base/draggable.css"; +import "jquery-ui/themes/base/resizable.css"; -let Note = { - HIDE_DELAY: 250, - NORMALIZE_ATTRIBUTES: ['letter-spacing', 'line-height', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom'], - COPY_ATTRIBUTES: ['background-color', 'border-radius', 'transform', 'justify-content', 'align-items'], - permitted_style_values: function(attribute, $attribute_child) { - if ($attribute_child.length === 0) { - return ""; - } - let found_attribute = $attribute_child.attr('style').split(';').filter(val => val.match(RegExp(`(^| )${attribute}:`))); - if (found_attribute.length === 0) { - return ""; - } - let [, value] = found_attribute[0].trim().split(':').map(val => val.trim()); - if (attribute === "background-color") { - const color_code = $attribute_child.css('background-color'); - return color_code.startsWith('rgba') ? "" : value; - } - if (attribute === "transform") { - let rotate_match = value.match(/rotate\([^)]+\)/); - return rotate_match ? rotate_match[0] : ""; - } - return value; - }, +import CurrentUser from './current_user'; +import Utility, { clamp } from './utility'; - Box: { - create: function(id) { - var $inner_border = $('
'); - $inner_border.addClass("note-box-inner-border"); +class Note { + static HIDE_DELAY = 250; + static NORMALIZE_ATTRIBUTES = ['letter-spacing', 'line-height', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom']; + static COPY_ATTRIBUTES = ['background-color', 'border-radius', 'transform', 'justify-content', 'align-items']; + static RESIZE_HANDLES = "se, nw"; - var $note_box = $('
'); - $note_box.addClass("note-box"); + // Notes must be at least 10x10 in size so they're big enough to drag and resize. + static MIN_NOTE_SIZE = 10; - if (Note.embed) { - $note_box.addClass("embedded"); + static notes = []; + static timeouts = []; + + id = null; + box = null; + body = null; + $note_container = null; + has_rotation = false; + + static Box = class { + note = null; + $note_box = null; + $inner_border = null; + + constructor(note) { + this.note = note; + this.$note_box = $('
'); + this.note.$note_container.append(this.$note_box); + + if (note.embed) { + this.$note_box.addClass("embedded"); + this.$inner_border = $('
'); + this.$note_box.append(this.$inner_border); } - $note_box.data("id", String(id)); - $note_box.attr("data-id", String(id)); - $note_box.draggable({ - containment: $("#image"), - stop: function(e, ui) { - Note.Box.update_data_attributes($note_box); - } + if (this.note.is_new()) { + this.$note_box.addClass("unsaved"); + } + + this.$note_box.draggable({ + containment: this.note.$note_container, }); - $note_box.resizable({ - containment: $("#image"), - handles: "se, nw", - stop: function(e, ui) { - Note.Box.update_data_attributes($note_box); - } + + this.$note_box.resizable({ + containment: this.note.$note_container, + handles: Note.RESIZE_HANDLES, + minWidth: Note.MIN_NOTE_SIZE, + minHeight: Note.MIN_NOTE_SIZE, }); - $note_box.css({position: "absolute"}); - $note_box.append($inner_border); - Note.Box.bind_events($note_box); - return $note_box; - }, + this.$note_box.on("click.danbooru", this.on_click.bind(this)); + this.$note_box.on("mouseenter.danbooru", this.on_mouseenter.bind(this)); + this.$note_box.on("mouseleave.danbooru", this.on_mouseleave.bind(this)); + this.$note_box.on("dragstart.danbooru resizestart.danbooru", this.on_dragstart.bind(this)); + this.$note_box.on("dragstop.danbooru resizestop.danbooru", this.on_dragstop.bind(this)); + } - update_data_attributes: function($note_box) { - var $image = $("#image"); - var ratio = $image.width() / parseFloat($image.data("original-width")); - var new_x = parseFloat($note_box.css("left")); - var new_y = parseFloat($note_box.css("top")); - var new_width = parseFloat($note_box.css("width")); - var new_height = parseFloat($note_box.css("height")); - new_x = parseInt(new_x / ratio); - new_y = parseInt(new_y / ratio); - new_width = parseInt(new_width / ratio); - new_height = parseInt(new_height / ratio); - $note_box.data("x", new_x); - $note_box.data("y", new_y); - $note_box.data("width", new_width); - $note_box.data("height", new_height); - }, + on_click() { + if (!Utility.test_max_width(660)) { + // Toggle selection status when note is clicked. Enable movement keys when note is selected. + if (this.$note_box.hasClass("movable")) { + this.$note_box.removeClass("movable"); + $(document).off("keydown.nudge_note"); + $(document).off("keydown.resize_note"); + } else { + this.$note_box.addClass("movable"); + Utility.keydown("up down left right", "nudge_note", this.key_nudge.bind(this)); + Utility.keydown("shift+up shift+down shift+left shift+right", "resize_note", this.key_resize.bind(this)); + } + } else if (this.$note_box.hasClass("viewing")) { + this.note.body.hide(); + this.$note_box.removeClass("viewing"); + } else { + $(".note-box").removeClass("viewing"); + this.note.body.show(); + this.$note_box.addClass("viewing"); + } + } - copy_style_attributes: function($note_box) { - const $attribute_child = $note_box.find('.note-box-attributes'); + on_mouseenter() { + this.note.body.show(); + this.$note_box.addClass("hovering"); + } + + on_mouseleave() { + this.note.body.hide(); + this.$note_box.removeClass("hovering"); + } + + on_dragstart() { + this.$note_box.addClass("unsaved"); + Note.Body.hide_all(); + } + + // Reset the note box placement after the box is dragged or resized. Dragging the note + // changes the CSS coordinates to pixels, so we have to convert them back to percentages. + on_dragstop() { + this.place_note(this.note.x, this.note.y, this.note.width, this.note.height); + } + + // Place the note box using absolute percentage coordinates (floats in the range 0.0..1.0). + place_note(x, y, w, h) { + if (this.note.embed && this.note.has_rotation) { + let position = this.get_min_max_position(); + x = position.norm_left / this.note.image_width; + y = position.norm_top / this.note.image_height; + } + + x = clamp(x, 0.0, 1.0); + y = clamp(y, 0.0, 1.0); + w = clamp(w, Note.MIN_NOTE_SIZE / this.note.post_width, 1.0); + h = clamp(h, Note.MIN_NOTE_SIZE / this.note.post_height, 1.0); + + this.$note_box.css({ + top: (100 * y) + '%', + left: (100 * x) + '%', + width: (100 * w) + '%', + height: (100 * h) + '%', + }); + } + + copy_style_attributes() { + let $note_box = this.$note_box; + let $attribute_child = $note_box.find('.note-box-attributes'); let has_rotation = false; + Note.COPY_ATTRIBUTES.forEach((attribute)=>{ - const attribute_value = Note.permitted_style_values(attribute, $attribute_child); + const attribute_value = this.permitted_style_values(attribute, $attribute_child); $note_box.css(attribute, attribute_value); + if (attribute === "transform" && attribute_value.startsWith("rotate")) { has_rotation = true; } }); + if (has_rotation) { const current_left = Math.round(parseFloat($note_box.css("left"))); const current_top = Math.round(parseFloat($note_box.css("top"))); - const position = Note.Box.get_min_max_position($note_box); + const position = this.get_min_max_position(); + // Checks for the scenario where the user sets invalid box values through the API // or by adjusting the box dimensions through the browser's dev console before saving if (current_left !== position.norm_left || current_top !== position.norm_top) { @@ -97,135 +152,48 @@ let Note = { top: position.percent_top, left: position.percent_left, }); + $note_box.addClass("out-of-bounds"); } else { $note_box.removeClass("out-of-bounds"); } - $note_box.data('has_rotation', true); + + this.note.has_rotation = true; } else { - $note_box.data('has_rotation', false); + this.note.has_rotation = false; } - }, + } - bind_events: function($note_box) { - $note_box.on( - "dragstart.danbooru resizestart.danbooru", - function(e) { - var $note_box_inner = $(e.currentTarget); - $note_box_inner.addClass("unsaved"); - Note.dragging = true; - Note.clear_timeouts(); - Note.Body.hide_all(); - e.stopPropagation(); - } - ); - - $note_box.on( - "dragstop.danbooru resizestop.danbooru", - function(e) { - Note.dragging = false; - e.stopPropagation(); - } - ); - - $note_box.on( - "mouseover.danbooru mouseout.danbooru", - function(e) { - if (Note.dragging || Utility.test_max_width(660)) { - return; - } - - var $this = $(this); - var $note_box_inner = $(e.currentTarget); - - const note_id = $note_box_inner.data("id"); - if (e.type === "mouseover") { - Note.Body.show(note_id); - if (Note.editing) { - $this.resizable("enable"); - $this.draggable("enable"); - } - $note_box.addClass("hovering"); - } else if (e.type === "mouseout") { - Note.Body.hide(note_id); - if (Note.editing) { - $this.resizable("disable"); - $this.draggable("disable"); - } - $note_box.removeClass("hovering"); - } - - e.stopPropagation(); - } - ); - - $note_box.on( - "click.danbooru", - function (event) { - const note_id = $note_box.data("id"); - if (!Utility.test_max_width(660)) { - $(".note-box").removeClass("movable"); - if (note_id === Note.move_id) { - Note.move_id = null; - } else { - Note.move_id = note_id; - $note_box.addClass("movable"); - } - } else if ($note_box.hasClass("viewing")) { - Note.Body.hide(note_id); - $note_box.removeClass("viewing"); - } else { - $(".note-box").removeClass("viewing"); - Note.Body.show(note_id); - $note_box.addClass("viewing"); - } - } - ); - - $note_box.on("mousedown.danbooru", function(event) { - Note.drag_id = $note_box.data('id'); - }); - - $note_box.on("mouseup.danbooru.drag", Note.Box.drag_stop); - }, - - find: function(id) { - return $(".note-container div.note-box[data-id=" + id + "]"); - }, - - drag_stop: function(event) { - if (Note.drag_id !== null) { - const $image = $("#image"); - const $note_box = Note.Box.find(Note.drag_id); - const dimensions = { - top: (100 * ($note_box.position().top / $image.height())) + '%', - left: (100 * ($note_box.position().left / $image.width())) + '%', - height: (100 * ($note_box.height() / $image.height())) + '%', - width: (100 * ($note_box.width() / $image.width())) + '%', - }; - if (Note.embed && $note_box.data('has_rotation')) { - const position = Note.Box.get_min_max_position($note_box); - Object.assign(dimensions, { - top: position.percentage_top, - left: position.percentage_left, - }); - } - $note_box.css(dimensions); - Note.drag_id = null; + permitted_style_values(attribute, $attribute_child) { + if ($attribute_child.length === 0) { + return ""; } - }, - key_nudge: function(event) { - if (!Note.move_id) { - return; + let found_attribute = $attribute_child.attr('style').split(';').filter(val => val.match(RegExp(`(^| )${attribute}:`))); + if (found_attribute.length === 0) { + return ""; } - const $note_box = Note.Box.find(Note.move_id); - if ($note_box.length === 0) { - return; + + let [, value] = found_attribute[0].trim().split(':').map(val => val.trim()); + if (attribute === "background-color") { + const color_code = $attribute_child.css('background-color'); + return color_code.startsWith('rgba') ? "" : value; } - let computed_style = window.getComputedStyle($note_box[0]); - let current_top = Math.round(parseFloat(computed_style.top)); - let current_left = Math.round(parseFloat(computed_style.left)); + + if (attribute === "transform") { + let rotate_match = value.match(/rotate\([^)]+\)/); + return rotate_match ? rotate_match[0] : ""; + } + + return value; + } + + key_nudge(event) { + let $note_box = this.note.box.$note_box; + + let current_top = Math.round($note_box.position().top); + let current_left = Math.round($note_box.position().left); + switch (event.originalEvent.key) { case "ArrowUp": current_top--; @@ -242,28 +210,25 @@ let Note = { default: // do nothing } - let position = Note.Box.get_min_max_position($note_box, current_top, current_left); + + let position = this.get_min_max_position(current_top, current_left); $note_box.css({ top: position.percent_top, left: position.percent_left, }); + $note_box.addClass("unsaved"); event.preventDefault(); - }, + } - key_resize: function (event) { - if (!Note.move_id) { - return; - } - const $note_box = Note.Box.find(Note.move_id); - if ($note_box.length === 0) { - return; - } - let computed_style = window.getComputedStyle($note_box[0]); - let current_top = Math.round(parseFloat(computed_style.top)); - let current_left = Math.round(parseFloat(computed_style.left)); + key_resize(event) { + let $note_box = this.note.box.$note_box; + + let current_top = Math.round($note_box.position().top); + let current_left = Math.round($note_box.position().left); let current_height = $note_box.height(); let current_width = $note_box.width(); + switch (event.originalEvent.key) { case "ArrowUp": current_height--; @@ -280,27 +245,32 @@ let Note = { default: // do nothing } - const position = Note.Box.get_min_max_position($note_box, null, null, current_height, current_width); + + const position = this.get_min_max_position(null, null, current_height, current_width); if (current_top === position.norm_top && current_left === position.norm_left) { $note_box.css({ height: current_height, width: current_width, }); } + $note_box.addClass("unsaved"); event.preventDefault(); - }, + } - get_min_max_position: function($note_box, current_top = null, current_left = null, current_height = null, current_width = null) { + get_min_max_position(current_top = null, current_left = null, current_height = null, current_width = null) { + let $note_box = this.$note_box; const computed_style = window.getComputedStyle($note_box[0]); + current_top = (current_top === null ? parseFloat(computed_style.top) : current_top); current_left = (current_left === null ? parseFloat(computed_style.left) : current_left); current_height = current_height || $note_box.height(); current_width = current_width || $note_box.width(); - const $image = $("#image"); - const image_height = $image.height(); - const image_width = $image.width(); - const box_data = Note.Box.get_bounding_box($note_box, current_height, current_width); + + const image_height = this.note.image_height; + const image_width = this.note.image_width; + const box_data = this.get_bounding_box(current_height, current_width); + if (((box_data.max_x - box_data.min_x) <= image_width) && ((box_data.max_y - box_data.min_y) <= image_height)) { current_top = Math.min(Math.max(current_top, -box_data.min_y, 0), image_height - box_data.max_y - 2, image_height - box_data.min_y - box_data.max_y - 2, image_height); current_left = Math.min(Math.max(current_left, -box_data.min_x, 0), image_width - box_data.max_x - 2, image_width - box_data.min_x - box_data.max_x - 2, image_width); @@ -308,20 +278,23 @@ let Note = { Utility.error("Box too large to be rotated!"); $note_box.css('transform', 'none'); } + return { norm_top: Math.round(current_top), norm_left: Math.round(current_left), - percent_top: (100 * (current_top / $image.height())) + '%', - percent_left: (100 * (current_left / $image.width())) + '%', + percent_top: (100 * (current_top / image_height)) + '%', + percent_left: (100 * (current_left / image_width)) + '%', }; - }, + } - get_bounding_box: function($note_box, height = null, width = null) { + get_bounding_box(height = null, width = null) { + let $note_box = this.$note_box; height = height || $note_box.height(); width = width || $note_box.width(); let old_coord = [[0, 0], [width, 0], [0, height], [width, height]]; const computed_style = window.getComputedStyle($note_box[0]); const match = computed_style.transform.match(/matrix\(([-e0-9.]+), ([-e0-9.]+)/); + if (!match) { return { min_x: 0, @@ -332,6 +305,7 @@ let Note = { degrees: 0, } } + const costheta = Math.round(match[1] * 1000) / 1000; const sintheta = Math.round(match[2] * 1000) / 1000; let trans_x = width / 2; @@ -340,6 +314,7 @@ let Note = { let max_x = 0; let min_y = Infinity; let max_y = 0; + const new_coord = old_coord.map((coord)=>{ let temp_x = coord[0] - trans_x; let temp_y = coord[1] - trans_y; @@ -353,11 +328,14 @@ let Note = { max_y = Math.max(max_y, new_y); return [new_x, new_y]; }); + const norm_coord = new_coord.map((coord)=>{ return [coord[0] - min_x, coord[1] - min_y]; }); + const radians_per_degree = 0.017453292519943295; const degrees = Math.asin(sintheta) / radians_per_degree; + return { min_x: min_x, min_y: min_y, @@ -366,72 +344,62 @@ let Note = { norm_coord: norm_coord, degrees: degrees, }; - }, + } - show_highlighted: function($note_box) { - var note_id = $note_box.data("id"); - - Note.Body.show(note_id); + show_highlighted() { + this.note.body.show(); $(".note-box-highlighted").removeClass("note-box-highlighted"); - $note_box.addClass("note-box-highlighted"); - $note_box[0].scrollIntoView(false); - }, + this.$note_box.addClass("note-box-highlighted"); + this.$note_box[0].scrollIntoView(false); + } - scale: function($note_box) { - var $image = $("#image"); - var original_width = $image.data("original-width"); - var original_height = $image.data("original-height"); - var x_percent = 100 * ($note_box.data('x') / original_width); - var y_percent = 100 * ($note_box.data('y') / original_height); - var height_percent = 100 * ($note_box.data('height') / original_height); - var width_percent = 100 * ($note_box.data('width') / original_width); - $note_box.css({ - top: y_percent + '%', - left: x_percent + '%', - width: width_percent + '%', - height: height_percent + '%', - }); - }, + // Rescale font sizes of embedded notes when the image is resized. + static scale_all() { + let $image = $("#image"); + let $container = $(".note-container"); - scale_all: function() { - const $container = $('.note-container'); if ($container.length === 0) { return; } - let $image = $("#image"); - if (Note.embed) { - let large_width = parseFloat($image.data('large-width')); - let ratio = $image.width() / large_width; - let font_percentage = ratio * 100; - $container.css('font-size', font_percentage + '%'); - } - }, - toggle_all: function() { + let large_width = parseFloat($image.data('large-width')); + let ratio = $image.width() / large_width; + let font_percentage = ratio * 100; + + $container.css('font-size', font_percentage + '%'); + } + + static toggle_all() { $(".note-container").toggleClass("hide-notes"); } - }, + } - Body: { - create: function(id) { - var $note_body = $('
'); - $note_body.addClass("note-body"); - $note_body.data("id", String(id)); - $note_body.attr("data-id", String(id)); - $note_body.hide(); - Note.Body.bind_events($note_body); - return $note_body; - }, + static Body = class { + note = null; + $note_body = null; - initialize: function($note_body) { - var $note_box = Note.Box.find($note_body.data("id")); - if (Note.embed && $note_box.data('has_rotation')) { - const box_data = Note.Box.get_bounding_box($note_box); + constructor(note) { + this.note = note; + this.$note_body = $('
'); + this.note.$note_container.append(this.$note_body); + + this.$note_body.on("mouseover.danbooru", this.on_mouseover.bind(this)); + this.$note_body.on("mouseout.danbooru", this.on_mouseout.bind(this)); + this.$note_body.on("click.danbooru", this.on_click.bind(this)); + } + + initialize() { + let $note_body = this.$note_body; + let $note_box = this.note.box.$note_box; + + if (this.note.embed && this.note.has_rotation) { + const box_data = this.note.box.get_bounding_box(); // Select the lowest box corner to the farthest left let selected_corner = box_data.norm_coord.reduce(function (selected, coord) {return (selected[1] > coord[1]) || (selected[1] === coord[1] && selected[0] < coord[0]) ? selected : coord;}); let normalized_degrees = box_data.degrees % 90.0; // Align to the left or right body corner depending upon the box angle let body_corner = $note_box.position().left - (normalized_degrees > 0.0 && normalized_degrees <= 45.0 ? $note_body.width() : 0); + $note_body.css({ top: $note_box.position().top + selected_corner[1] + 5, left: body_corner + selected_corner[0], @@ -442,46 +410,47 @@ let Note = { left: $note_box.position().left, }); } - Note.Body.bound_position($note_body); - }, - bound_position: function($note_body) { - var $image = $("#image"); + this.bound_position(); + } + + bound_position() { + var $image = this.note.$note_container; var doc_width = $image.offset().left + $image.width(); + let $note_body = this.$note_body; if ($note_body.offset().left + $note_body.width() > doc_width) { $note_body.css({ left: $note_body.position().left - 10 - ($note_body.offset().left + $note_body.width() - doc_width) }); } - }, + } - show: function(id) { + show() { Note.Body.hide_all(); - Note.clear_timeouts(); - var $note_body = Note.Body.find(id); - if (!$note_body.data('resized')) { - Note.Body.resize($note_body); - $note_body.data('resized', 'true'); + + if (!this.resized) { + this.resize(); + this.resized = true; } - $note_body.show(); - Note.Body.initialize($note_body); - }, - find: function(id) { - return $(".note-container div.note-body[data-id=" + id + "]"); - }, + this.$note_body.show(); + this.initialize(); + } - hide: function(id) { - var $note_body = Note.Body.find(id); - Note.timeouts.push(setTimeout(() => $note_body.hide(), Note.HIDE_DELAY)); - }, + hide(delay = Note.HIDE_DELAY) { + Note.timeouts.push(setTimeout(() => this.$note_body.hide(), delay)); + } - hide_all: function() { + static hide_all() { + Note.timeouts.forEach(clearTimeout); + Note.timeouts = []; $(".note-container div.note-body").hide(); - }, + } + + resize() { + let $note_body = this.$note_body; - resize: function($note_body) { $note_body.css("min-width", ""); var w = $note_body.width(); var h = $note_body.height(); @@ -525,75 +494,79 @@ let Note = { $note_body.css("min-width", hi); } } - }, + } + + display_text(text) { + this.set_text(text); + + if (this.note.embed) { + let $note_inner_box = this.note.box.$inner_border; - set_text: function($note_body, $note_box, text) { - if (Note.embed) { - const $note_inner_box = $note_box.find("div.note-box-inner-border"); - Note.Body.display_text($note_inner_box, text); // Reset the font size so that the normalization calculations will be correct - $note_inner_box.css("font-size", Note.base_font_size + "px"); - Note.normalize_sizes($note_inner_box.children(), Note.base_font_size); + $note_inner_box.css("font-size", this.note.base_font_size + "px"); + this.note.normalize_sizes($note_inner_box.children(), this.note.base_font_size); + // Clear the font size so that the fonts will be scaled to the current value $note_inner_box.css("font-size", ""); - Note.Box.copy_style_attributes($note_box); - } else { - Note.Body.display_text($note_body, text); + this.note.box.copy_style_attributes(); } - Note.Body.resize($note_body); - Note.Body.bound_position($note_body); - }, - display_text: function($note_body, text) { + this.resize(); + this.bound_position(); + } + + set_text(text) { + text = text ?? ""; text = text.replace(//g, '

'); text = text.replace(/<\/tn>/g, '

'); text = text.replace(/\n/g, '
'); - $note_body.html(text); - }, - bind_events: function($note_body) { - $note_body.on("mouseover.danbooru", function(e) { - var $note_body_inner = $(e.currentTarget); - Note.Body.show($note_body_inner.data("id")); - e.stopPropagation(); - }); - - $note_body.on("mouseout.danbooru", function(e) { - var $note_body_inner = $(e.currentTarget); - Note.Body.hide($note_body_inner.data("id")); - e.stopPropagation(); - }); - - if (CurrentUser.data("is-anonymous") === false) { - $note_body.on("click.danbooru", function(e) { - if (e.target.tagName !== "A") { - var $note_body_inner = $(e.currentTarget); - Note.Edit.show($note_body_inner); - } - e.stopPropagation(); - }); + if (this.note.embed) { + this.note.box.$inner_border.html(text); + this.$note_body.html("Click to edit"); + } else if (text) { + this.$note_body.html(text); } else { - $note_body.on("click.danbooru", function(e) { - if (e.target.tagName !== "A") { - Utility.notice("You must be logged in to edit notes"); - } - e.stopPropagation(); - }); + this.$note_body.html("Click to edit"); } } - }, - Edit: { - show: function($note_body) { - var id = $note_body.data("id"); + async preview_text(text) { + this.display_text("Loading..."); + let response = await $.getJSON("/note_previews", { body: text }); - if (Note.editing) { + this.display_text(response.body); + this.initialize(); + this.$note_body.show(); + } + + on_mouseover(e) { + this.show(); + } + + on_mouseout() { + this.hide(); + } + + on_click(e) { + // don't open the note edit dialog when the user clicks a link in the note body. + if ($(e.target).is("a")) { return; } - $(".note-box").resizable("disable"); - $(".note-box").draggable("disable"); - $(".note-box").addClass("editing"); + if (CurrentUser.data("is-anonymous")) { + Utility.notice("You must be logged in to edit notes"); + } else { + Note.Edit.show(this.note); + } + } + } + + static Edit = class { + static show(note) { + if ($(".note-box").hasClass("editing")) { + return; + } let $textarea = $(''); $textarea.css({ @@ -602,15 +575,16 @@ let Note = { resize: "none", }); - if (!$note_body.hasClass("new-note")) { - $textarea.val($note_body.data("original-body")); + if (!note.is_new()) { + $textarea.val(note.original_body); } let $dialog = $('
'); - let note_title = (typeof id === 'string' && id.startsWith('x') ? 'Creating new note' : 'Editing note #' + id); + let note_title = note.is_new() ? 'Creating new note' : `Editing note #${note.id}`; + $dialog.append('' + note_title + ' (view help)'); $dialog.append($textarea); - $dialog.data("id", id); + $dialog.dialog({ width: 360, height: 240, @@ -622,448 +596,321 @@ let Note = { classes: { "ui-dialog": "note-edit-dialog", }, + open: () => { + $(".note-box").addClass("editing"); + }, + close: () => { + $(".note-box").removeClass("editing"); + }, buttons: { - "Save": Note.Edit.save, - "Preview": Note.Edit.preview, - "Cancel": Note.Edit.cancel, - "Delete": Note.Edit.destroy, - "History": Note.Edit.history + "Save": () => Note.Edit.save($dialog, note), + "Preview": () => Note.Edit.preview($dialog, note), + "Cancel": () => Note.Edit.cancel($dialog, note), + "Delete": () => Note.Edit.destroy($dialog, note), + "History": () => Note.Edit.history($dialog, note), } }); - $dialog.data("uiDialog")._title = function(title) { - title.html(this.options.title); // Allow unescaped html in dialog title - } - - $dialog.on("dialogclose.danbooru", function() { - Note.editing = false; - $(".note-box").resizable("enable"); - $(".note-box").draggable("enable"); - $(".note-box").removeClass("editing"); - }); $textarea.selectEnd(); - Note.editing = true; - }, - - parameterize_note: function($note_box, $note_body) { - var $image = $("#image"); - var original_width = parseInt($image.data("original-width")); - var ratio = parseInt($image.width()) / original_width; - - var hash = { - note: { - x: Math.round($note_box.position().left / ratio), - y: Math.round($note_box.position().top / ratio), - width: Math.round($note_box.width() / ratio), - height: Math.round($note_box.height() / ratio), - body: $note_body.data("original-body"), - } - } - - if ($note_box.data("id").match(/x/)) { - hash.note.html_id = $note_box.data("id"); - hash.note.post_id = Utility.meta("post-id"); - } - - return hash; - }, - - error_handler: function(xhr, status, exception) { - Utility.error("Error: " + (xhr.responseJSON.reason || xhr.responseJSON.reasons.join("; "))); - }, - - success_handler: function(data, status, xhr) { - var $note_box = null; - - if (data.html_id) { // new note - var $note_body = Note.Body.find(data.html_id); - $note_box = Note.Box.find(data.html_id); - $note_body.data("id", String(data.id)).attr("data-id", data.id); - $note_box.data("id", String(data.id)).attr("data-id", data.id); - $note_box.removeClass("new-note"); - $note_box.removeClass("unsaved"); - $note_box.removeClass("movable"); - } else { - $note_box = Note.Box.find(data.id); - $note_box.removeClass("unsaved"); - $note_box.removeClass("movable"); - } - Note.move_id = null; - }, - - save: function() { - var $this = $(this); - var $textarea = $this.find("textarea"); - var id = $this.data("id"); - var $note_body = Note.Body.find(id); - var $note_box = Note.Box.find(id); - var text = $textarea.val(); - $note_body.data("original-body", text); - Note.Body.set_text($note_body, $note_box, "Loading..."); - $.get("/note_previews.json", {body: text}).then(function(data) { - Note.Body.set_text($note_body, $note_box, data.body); - Note.Body.initialize($note_body); - $note_body.show(); - }); - $this.dialog("close"); - - if (id.match(/\d/)) { - $.ajax("/notes/" + id + ".json", { - type: "PUT", - data: Note.Edit.parameterize_note($note_box, $note_body), - error: Note.Edit.error_handler, - success: Note.Edit.success_handler - }); - } else { - $.ajax("/notes.json", { - type: "POST", - data: Note.Edit.parameterize_note($note_box, $note_body), - error: Note.Edit.error_handler, - success: Note.Edit.success_handler - }); - } - }, - - preview: function() { - var $this = $(this); - var $textarea = $this.find("textarea"); - var id = $this.data("id"); - var $note_body = Note.Body.find(id); - var text = $textarea.val(); - var $note_box = Note.Box.find(id); - $note_box.addClass("unsaved"); - Note.Body.set_text($note_body, $note_box, "Loading..."); - $.get("/note_previews.json", {body: text}).then(function(data) { - Note.Body.set_text($note_body, $note_box, data.body); - Note.Body.initialize($note_body); - $note_body.show(); - }); - }, - - cancel: function() { - $(this).dialog("close"); - }, - - destroy: function() { - if (!confirm("Do you really want to delete this note?")) { - return - } - - var $this = $(this); - var id = $this.data("id"); - - if (id.match(/\d/)) { - $.ajax("/notes/" + id + ".json", { - type: "DELETE", - success: function() { - Note.Box.find(id).remove(); - Note.Body.find(id).remove(); - $this.dialog("close"); - } - }); - } - }, - - history: function() { - var $this = $(this); - var id = $this.data("id"); - if (id.match(/\d/)) { - window.location.href = "/note_versions?search[note_id]=" + id; - } - $(this).dialog("close"); } - }, - TranslationMode: { - active: false, + static async save($dialog, note) { + let $note_box = note.box.$note_box; + let text = $dialog.find("textarea").val(); - toggle: function(e) { - if (Note.TranslationMode.active) { - Note.TranslationMode.stop(e); + let params = { + x: Math.round(note.x * note.post_width), + y: Math.round(note.y * note.post_height), + width: Math.round(note.width * note.post_width), + height: Math.round(note.height * note.post_height), + body: text + }; + + note.original_body = text; + note.body.preview_text(text); + + try { + if (note.is_new()) { + params.post_id = note.post_id; + let response = await $.ajax("/notes.json", { type: "POST", data: { note: params }}); + note.id = response.id; + } else { + await $.ajax(`/notes/${note.id}.json`, { type: "PUT", data: { note: params }}); + } + + $dialog.dialog("close"); + $note_box.removeClass("unsaved"); + } catch (xhr) { + Utility.error("Error: " + (xhr.responseJSON.reason || xhr.responseJSON.reasons.join("; "))); + } + } + + static async preview($dialog, note) { + let text = $dialog.find("textarea").val(); + + note.box.$note_box.addClass("unsaved"); + note.body.preview_text(text); + } + + static cancel($dialog, _note) { + $dialog.dialog("close"); + } + + static async destroy($dialog, note) { + if (!confirm("Do you really want to delete this note?")) { + return; + } + + if (!note.is_new()) { + await $.ajax(`/notes/${note.id}.json`, { type: "DELETE" }); + note.box.$note_box.remove(); + note.body.$note_body.remove(); + $dialog.dialog("close"); + } + } + + static history($dialog, note) { + if (!note.is_new()) { + window.location.href = `/note_versions?search[note_id]=${note.id}`; + } + + $dialog.dialog("close"); + } + } + + static TranslationMode = class { + static toggle() { + if ($("body").hasClass("mode-translation")) { + Note.TranslationMode.stop(); } else { - Note.TranslationMode.start(e); + Note.TranslationMode.start(); } - }, + } - start: function(e) { - e.preventDefault(); - - if (CurrentUser.data("is-anonymous")) { - Utility.notice("You must be logged in to edit notes"); - return; - } - - if (Note.TranslationMode.active) { - return; - } - - $("#image").css("cursor", "crosshair"); - Note.TranslationMode.active = true; + static start() { $(document.body).addClass("mode-translation"); $("#image").off("click.danbooru", Note.Box.toggle_all); $("#image").on("mousedown.danbooru.note", Note.TranslationMode.Drag.start); - $(document).on("mouseup.danbooru.note", Note.TranslationMode.Drag.stop); - $("#mark-as-translated-section").show(); Utility.notice('Translation mode is on. Drag on the image to create notes. Turn translation mode off (shortcut is n).'); $("#notice a:contains(Turn translation mode off)").on("click.danbooru", Note.TranslationMode.stop); - }, + } - stop: function(e) { - e.preventDefault(); - - Note.TranslationMode.active = false; - $("#image").css("cursor", "auto"); + static stop() { + $("#note-preview").hide(); $("#image").on("click.danbooru", Note.Box.toggle_all); $("#image").off("mousedown.danbooru.note", Note.TranslationMode.Drag.start); - $(document).off("mouseup.danbooru.note", Note.TranslationMode.Drag.stop); + $(document).off("mouseup.danbooru", Note.TranslationMode.Drag.stop); + $(document).off("mousemove.danbooru", Note.TranslationMode.Drag.drag); $(document.body).removeClass("mode-translation"); $("#close-notice-link").click(); - $("#mark-as-translated-section").hide(); - }, + } - create_note: function(e, x, y, w, h) { - if (w > 9 || h > 9) { /* minimum note size: 10px */ - if (w <= 9) { - w = 10; - } else if (h <= 9) { - h = 10; - } - Note.create(x, y, w, h); - } + static Drag = class { + static dragStartX = 0; + static dragStartY = 0; - $(".note-container").removeClass("hide-notes"); - e.stopPropagation(); - e.preventDefault(); - }, - - Drag: { - dragging: false, - dragStartX: 0, - dragStartY: 0, - dragDistanceX: 0, - dragDistanceY: 0, - x: 0, - y: 0, - w: 0, - h: 0, - - start: function (e) { + static start(e) { if (e.which !== 1) { return; } + e.preventDefault(); /* don't drag the image */ $(document).on("mousemove.danbooru", Note.TranslationMode.Drag.drag); + $(document).on("mouseup.danbooru", Note.TranslationMode.Drag.stop); Note.TranslationMode.Drag.dragStartX = e.pageX; Note.TranslationMode.Drag.dragStartY = e.pageY; - }, + } - drag: function (e) { - Note.TranslationMode.Drag.dragDistanceX = e.pageX - Note.TranslationMode.Drag.dragStartX; - Note.TranslationMode.Drag.dragDistanceY = e.pageY - Note.TranslationMode.Drag.dragStartY; + static drag(e) { var $image = $("#image"); var offset = $image.offset(); - var limitX1 = $image.width() - Note.TranslationMode.Drag.dragStartX + offset.left - 1; - var limitX2 = offset.left - Note.TranslationMode.Drag.dragStartX; - var limitY1 = $image.height() - Note.TranslationMode.Drag.dragStartY + offset.top - 1; - var limitY2 = offset.top - Note.TranslationMode.Drag.dragStartY; - if (Note.TranslationMode.Drag.dragDistanceX > limitX1) { - Note.TranslationMode.Drag.dragDistanceX = limitX1; - } else if (Note.TranslationMode.Drag.dragDistanceX < limitX2) { - Note.TranslationMode.Drag.dragDistanceX = limitX2; + // (x0, y0) is the top left point of the drag box. (x1, y1) is the bottom right point. + let x0 = clamp(e.pageX, offset.left, Note.TranslationMode.Drag.dragStartX); + let y0 = clamp(e.pageY, offset.top, Note.TranslationMode.Drag.dragStartY); + let x1 = clamp(e.pageX, Note.TranslationMode.Drag.dragStartX, offset.left + $image.width()); + let y1 = clamp(e.pageY, Note.TranslationMode.Drag.dragStartY, offset.top + $image.height()); + + // Convert from page-relative coordinates to image-relatives coordinates. + let x = x0 - offset.left; + let y = y0 - offset.top; + let w = x1 - x0; + let h = y1 - y0; + + // Only show the new note box after we've dragged a minimum distance. This is to avoid + // accidentally creating tiny notes if we drag a small distance while trying to toggle notes. + if (w >= Note.MIN_NOTE_SIZE || h >= Note.MIN_NOTE_SIZE) { + $("#note-preview").show(); } - if (Note.TranslationMode.Drag.dragDistanceY > limitY1) { - Note.TranslationMode.Drag.dragDistanceY = limitY1; - } else if (Note.TranslationMode.Drag.dragDistanceY < limitY2) { - Note.TranslationMode.Drag.dragDistanceY = limitY2; + if ($("#note-preview").is(":visible")) { + $('#note-preview').css({ left: x, top: y, width: w, height: h }); } + } - if (Math.abs(Note.TranslationMode.Drag.dragDistanceX) > 9 && Math.abs(Note.TranslationMode.Drag.dragDistanceY) > 9) { - Note.TranslationMode.Drag.dragging = true; /* must drag at least 10pixels (minimum note size) in both dimensions. */ - } - if (Note.TranslationMode.Drag.dragging) { - if (Note.TranslationMode.Drag.dragDistanceX >= 0) { - Note.TranslationMode.Drag.x = Note.TranslationMode.Drag.dragStartX - offset.left; - Note.TranslationMode.Drag.w = Note.TranslationMode.Drag.dragDistanceX; - } else { - Note.TranslationMode.Drag.x = Note.TranslationMode.Drag.dragStartX - offset.left + Note.TranslationMode.Drag.dragDistanceX; - Note.TranslationMode.Drag.w = -Note.TranslationMode.Drag.dragDistanceX; - } - - if (Note.TranslationMode.Drag.dragDistanceY >= 0) { - Note.TranslationMode.Drag.y = Note.TranslationMode.Drag.dragStartY - offset.top; - Note.TranslationMode.Drag.h = Note.TranslationMode.Drag.dragDistanceY; - } else { - Note.TranslationMode.Drag.y = Note.TranslationMode.Drag.dragStartY - offset.top + Note.TranslationMode.Drag.dragDistanceY; - Note.TranslationMode.Drag.h = -Note.TranslationMode.Drag.dragDistanceY; - } - - $('#note-preview').css({ - display: 'block', - left: (Note.TranslationMode.Drag.x + 1), - top: (Note.TranslationMode.Drag.y + 1), - width: (Note.TranslationMode.Drag.w - 3), - height: (Note.TranslationMode.Drag.h - 3) - }); - } - }, - - stop: function (e) { - if (e.which !== 1) { - return; - } - if (Note.TranslationMode.Drag.dragStartX === 0) { - return; /* 'stop' is bound to window, don't create note if start wasn't triggered */ - } + static stop() { $(document).off("mousemove.danbooru", Note.TranslationMode.Drag.drag); + $(document).off("mouseup.danbooru", Note.TranslationMode.Drag.stop); - if (Note.TranslationMode.Drag.dragging) { - $('#note-preview').css({ display: 'none' }); - Note.TranslationMode.create_note(e, Note.TranslationMode.Drag.x, Note.TranslationMode.Drag.y, Note.TranslationMode.Drag.w - 1, Note.TranslationMode.Drag.h - 1); - Note.TranslationMode.Drag.dragging = false; /* border of the note is pixel-perfect on the preview border */ - } else { /* no dragging -> toggle display of notes */ + if ($("#note-preview").is(":visible")) { + let $image = $("#image"); + + new Note({ + x: $("#note-preview").position().left / $image.width(), + y: $("#note-preview").position().top / $image.height(), + w: $("#note-preview").width() / $image.width(), + h: $("#note-preview").height() / $image.height(), + }); + + $("#note-preview").hide(); + $(".note-container").removeClass("hide-notes"); + } else { /* If we didn't drag far enough, treat it as a click and toggle displaying notes. */ Note.Box.toggle_all(); } - - Note.TranslationMode.Drag.dragStartX = 0; - Note.TranslationMode.Drag.dragStartY = 0; } } - }, + } - id: "x", - dragging: false, - editing: false, - move_id: null, - drag_id: null, - base_font_size: null, - timeouts: [], - pending: {}, + constructor({ x, y, w, h, id = null, original_body = null, sanitized_body = null } = {}) { + this.$note_container = $(".note-container"); - add: function(container, id, x, y, w, h, original_body, sanitized_body) { - var $note_box = Note.Box.create(id); - var $note_body = Note.Body.create(id); + this.id = id; + this.post_id = this.$note_container.data("id"); + this.embed = Utility.meta("post-has-embedded-notes") === "true"; + this.original_body = original_body; - $note_box.data('x', x); - $note_box.data('y', y); - $note_box.data('width', w); - $note_box.data('height', h); - container.appendChild($note_box[0]); - container.appendChild($note_body[0]); - $note_body.data("original-body", original_body); - Note.Box.scale($note_box); - if (Note.embed) { - Note.Body.display_text($note_box.children("div.note-box-inner-border"), sanitized_body); - Note.Body.display_text($note_body, "Click to edit"); - } else { - Note.Body.display_text($note_body, sanitized_body); - } - }, + this.box = new Note.Box(this); + this.body = new Note.Body(this); - create: function(x, y, w, h) { - var $note_box = Note.Box.create(Note.id); - var $note_body = Note.Body.create(Note.id); - $note_box.css({ - top: y, - left: x, - width: w, - height: h - }); - Note.Box.update_data_attributes($note_box); - Note.Box.scale($note_box); - $note_box.addClass("new-note"); - $note_box.addClass("unsaved"); - $note_body.html("Click to edit"); - $(".note-container").append($note_box); - $(".note-container").append($note_body); - Note.id += "x"; - }, + this.box.place_note(x, y, w, h); + this.body.display_text(sanitized_body); - normalize_sizes: function ($note_elements, parent_font_size) { + Note.notes.push(this); + } + + is_new() { + return this.id === null; + } + + // The coordinates of the top left corner of the note box, as floats in the + // range 0.0 to 1.0. Does not account for note rotation. + get x() { + return this.box.$note_box.position().left / this.image_width; + } + + get y() { + return this.box.$note_box.position().top / this.image_height; + } + + // The current width and height of the note box, as floats in the range 0.0 to 1.0. + get width() { + return this.box.$note_box.width() / this.image_width; + } + + get height() { + return this.box.$note_box.height() / this.image_height; + } + + // The width and height of the full-size original image in pixels. + get post_width() { + return parseInt(this.$note_container.attr("data-width")); + } + + get post_height() { + return parseInt(this.$note_container.attr("data-height")); + } + + // The current width and height of the image in pixels. Will be smaller than the post width + // if the sample image is being displayed, or if the image is resized to fit the screen. + get image_width() { + return parseInt(this.$note_container.width()); + } + + get image_height() { + return parseInt(this.$note_container.height()); + } + + // The initial font size of the note container. Embedded notes are scaled relative to this value. + get base_font_size() { + return parseFloat(this.$note_container.parent().css("font-size")); + } + + normalize_sizes($note_elements, parent_font_size) { if ($note_elements.length === 0) { return; } - $note_elements.each(function(i, element) { + + $note_elements.toArray().forEach((element) => { const $element = $(element); const computed_styles = window.getComputedStyle(element); const font_size = parseFloat(computed_styles.fontSize); - Note.NORMALIZE_ATTRIBUTES.forEach(function(attribute) { + + Note.NORMALIZE_ATTRIBUTES.forEach((attribute) => { const original_size = parseFloat(computed_styles[attribute]) || 0; const relative_em = original_size / font_size; $element.css(attribute, relative_em + "em"); }); + const font_percentage = 100 * (font_size / parent_font_size); $element.css("font-size", font_percentage + "%"); $element.attr("size", ""); - Note.normalize_sizes($element.children(), font_size); + + this.normalize_sizes($element.children(), font_size); }); - }, + } - clear_timeouts: function() { - Note.timeouts.forEach(clearTimeout); - Note.timeouts = []; - }, + static find(id) { + return Note.notes.find(note => note.id === id); + } - load_all: function() { - var fragment = document.createDocumentFragment(); - $.each($("#notes article"), function(i, article) { + static load_all() { + let $image = $(".image-container"); + + $("#notes article").toArray().forEach(article => { var $article = $(article); - Note.add( - fragment, - $article.data("id"), - $article.data("x"), - $article.data("y"), - $article.data("width"), - $article.data("height"), - $article.data("body"), - $article.html() - ); - }); - const $note_container = $(".note-container"); - $note_container.append(fragment); - if (Note.embed) { - Note.base_font_size = parseFloat(window.getComputedStyle($note_container[0]).fontSize); - $.each($(".note-box"), function(i, note_box) { - const $note_box = $(note_box); - Note.normalize_sizes($("div.note-box-inner-border", note_box).children(), Note.base_font_size); - // Accounting for transformation values calculations which aren't correct immediately on page load - setTimeout(()=>{Note.Box.copy_style_attributes($note_box);}, 100); - }); - } - Note.Box.scale_all(); - }, - initialize_all: function() { + new Note({ + id: $article.data("id"), + x: $article.data("x") / $image.data("width"), + y: $article.data("y") / $image.data("height"), + w: $article.data("width") / $image.data("width"), + h: $article.data("height") / $image.data("height"), + original_body: $article.data("body"), + sanitized_body: $article.html() + }); + }); + } + + static initialize_all() { if ($("#c-posts #a-show #image").length === 0 || $("video#image").length) { return; } - Note.embed = (Utility.meta("post-has-embedded-notes") === "true"); Note.load_all(); + Note.Box.scale_all(); + + $(document).on("click.danbooru", "#translate", (e) => { + Note.TranslationMode.toggle(); + e.preventDefault(); + }); - this.initialize_shortcuts(); this.initialize_highlight(); $(document).on("hashchange.danbooru.note", this.initialize_highlight); - Utility.keydown("up down left right", "nudge_note", Note.Box.key_nudge); - Utility.keydown("shift+up shift+down shift+left shift+right", "resize_note", Note.Box.key_resize); + $(window).on("resize.danbooru.note_scale", Note.Box.scale_all); - }, - - initialize_shortcuts: function() { - $("#translate").on("click.danbooru", Note.TranslationMode.toggle); $("#image").on("click.danbooru", Note.Box.toggle_all); - }, + } - initialize_highlight: function() { + static initialize_highlight() { var matches = window.location.hash.match(/^#note-(\d+)$/); if (matches) { - var $note_box = Note.Box.find(matches[1]); - Note.Box.show_highlighted($note_box); + let note_id = parseInt(matches[1]); + let note = Note.find(note_id); + note.box.show_highlighted(); } - }, + } } $(function() { diff --git a/app/javascript/src/javascripts/utility.js b/app/javascript/src/javascripts/utility.js index 4a335fd50..699278d7b 100644 --- a/app/javascript/src/javascripts/utility.js +++ b/app/javascript/src/javascripts/utility.js @@ -3,6 +3,10 @@ import Rails from '@rails/ujs'; let Utility = {}; +export function clamp(value, low, high) { + return Math.max(low, Math.min(value, high)); +} + Utility.delay = function(milliseconds) { return new Promise(resolve => setTimeout(resolve, milliseconds)); } diff --git a/app/javascript/src/styles/specific/notes.scss b/app/javascript/src/styles/specific/notes.scss index ba02223ad..6fd6c9a59 100644 --- a/app/javascript/src/styles/specific/notes.scss +++ b/app/javascript/src/styles/specific/notes.scss @@ -7,7 +7,9 @@ } div.note-body { + display: none; position: absolute; + font-size: 14px; border: var(--note-body-border); background: var(--note-body-background); color: var(--note-body-text-color); @@ -81,7 +83,7 @@ justify-content: center; align-items: center; text-align: center; - position: absolute; + position: absolute !important; border: var(--note-box-border); min-width: 5px; min-height: 5px; @@ -114,10 +116,6 @@ &.movable { opacity: 1; } - - div.ui-resizable-handle { - display: block; - } } &.editing, @@ -134,19 +132,25 @@ border: var(--movable-note-box-border); } - div.ui-resizable-handle { - display: none; + &:not(.hovering) div.ui-resizable-handle { + display: none !important; } } &.note-box-highlighted { outline: 2px solid var(--note-highlight-color); } + + div.ui-resizable-handle { + position: absolute; + } } } +/* the box that appears when dragging to create a new note. */ div#note-preview { position: absolute; + cursor: crosshair; border: var(--note-preview-border); opacity: 0.6; display: none; diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index f6dfee59f..c657cc29d 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -436,6 +436,14 @@ body[data-post-current-image-size="original"] #image-resize-notice { display: none; } +body.mode-translation .note-container { + cursor: crosshair; +} + +body:not(.mode-translation) div#c-posts div#a-show #mark-as-translated-section { + display: none; +} + div#c-post-versions, div#c-artist-versions { div#a-index { a { diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index 29d8467a9..b035b389c 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -61,7 +61,7 @@ <% end %> <% end %> -