From 9fbeb5ec3af968f03e8a5f0f2672e922c235731f Mon Sep 17 00:00:00 2001 From: BrokenEagle Date: Sat, 1 Feb 2020 00:05:51 +0000 Subject: [PATCH 1/5] Add note key nudge function - Notes must be clicked in order to engage the nudge function for that note -- This status is indicated by a green border around the note -- Clicking the note or another note will turn off nudging for that note - Also prevent notes from being nudged outside of the image borders --- app/javascript/src/javascripts/notes.js | 72 ++++++++++++++++++- app/javascript/src/styles/base/040_colors.css | 1 + app/javascript/src/styles/specific/notes.scss | 17 ++++- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index 4b24f67eb..f3c8f763a 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -95,15 +95,16 @@ let Note = { 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_box_inner.data("id")); + 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_box_inner.data("id")); + Note.Body.hide(note_id); if (Note.editing) { $this.resizable("disable"); $this.draggable("disable"); @@ -114,12 +115,76 @@ let Note = { e.stopPropagation(); } ); + + $note_box.on( + "click.danbooru", + function (event) { + const note_id = $note_box.data("id"); + $(".note-box").removeClass("movable"); + if (note_id === Note.move_id) { + Note.move_id = null; + } else { + Note.move_id = note_id; + $note_box.addClass("movable"); + } + } + ); }, find: function(id) { return $("#note-container div.note-box[data-id=" + id + "]"); }, + key_nudge: 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 = parseFloat(computed_style.top); + let current_left = parseFloat(computed_style.left); + switch (event.originalEvent.key) { + case "ArrowUp": + current_top--; + break; + case "ArrowDown": + current_top++; + break; + case "ArrowLeft": + current_left--; + break; + case "ArrowRight": + current_left++; + break; + default: + // do nothing + } + let position = Note.Box.get_min_max_position($note_box, current_top, current_left); + $note_box.css(position); + $note_box.find(".note-box-inner-border").addClass("unsaved"); + event.preventDefault(); + }, + + get_min_max_position: function($note_box, current_top = null, current_left = null, current_height = null, current_width = null) { + 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(); + current_top = Math.min(Math.max(current_top, 0), image_height - current_height - 2); + current_left = Math.min(Math.max(current_left, 0), image_width - current_width - 2); + return { + top: current_top, + left: current_left, + }; + }, + show_highlighted: function($note_box) { var note_id = $note_box.data("id"); @@ -697,9 +762,9 @@ let Note = { dragging: false, editing: false, base_font_size: null, + move_id: null, timeouts: [], pending: {}, - 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); @@ -798,6 +863,7 @@ let Note = { 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); }, initialize_shortcuts: function() { diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index d64d22c04..e8462604a 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -117,6 +117,7 @@ --note-box-background: transparent; --note-box-inner-border: 1px solid black; --unsaved-note-box-inner-border: 1px solid red; + --movable-note-box-inner-border: 1px solid green; --note-preview-border: 1px solid red; --note-preview-background: white; --note-highlight-color: blue; diff --git a/app/javascript/src/styles/specific/notes.scss b/app/javascript/src/styles/specific/notes.scss index 3b6e75be1..e7e14bc05 100644 --- a/app/javascript/src/styles/specific/notes.scss +++ b/app/javascript/src/styles/specific/notes.scss @@ -99,7 +99,8 @@ div#note-container { &.hovering { border: var(--note-box-border); - &.editing { + &.editing, + &.movable { opacity: 1; } @@ -112,10 +113,18 @@ div#note-container { } } - &.editing { + &.editing, + &.movable { opacity: 0.4; } + &.movable { + div.note-box-inner-border, + div.note-box-inner-border.unsaved { + border: var(--movable-note-box-inner-border); + } + } + div.ui-resizable-handle { display: none; } @@ -126,6 +135,10 @@ div#note-container { vertical-align: middle; border: 1px solid transparent; } + + div.note-box-inner-border.unsaved { + border: var(--unsaved-note-box-inner-border); + } } &.note-box-highlighted { From c082a258c78348a04682c59b6dc8c3be6a26f0be Mon Sep 17 00:00:00 2001 From: BrokenEagle Date: Sat, 1 Feb 2020 00:07:30 +0000 Subject: [PATCH 2/5] Add note key resize function - Same as using the key nudge, however it's used with the shift key --- app/javascript/src/javascripts/notes.js | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index f3c8f763a..1afc1a60c 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -168,6 +168,44 @@ let Note = { 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 current_height = $note_box.height(); + let current_width = $note_box.width(); + switch (event.originalEvent.key) { + case "ArrowUp": + current_height--; + break; + case "ArrowDown": + current_height++; + break; + case "ArrowLeft": + current_width--; + break; + case "ArrowRight": + current_width++; + break; + default: + // do nothing + } + const position = Note.Box.get_min_max_position($note_box, null, null, current_height, current_width); + $note_box.css({ + top: position.top, + left: position.left, + height: current_height, + width: current_width, + }); + Note.Box.resize_inner_border($note_box); + $note_box.find(".note-box-inner-border").addClass("unsaved"); + event.preventDefault(); + }, + get_min_max_position: function($note_box, current_top = null, current_left = null, current_height = null, current_width = null) { const computed_style = window.getComputedStyle($note_box[0]); current_top = (current_top === null ? parseFloat(computed_style.top) : current_top); @@ -864,6 +902,7 @@ let Note = { 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); }, initialize_shortcuts: function() { From 44f32a1e5e9f02f04c5a4bd1534b52273e63e3c1 Mon Sep 17 00:00:00 2001 From: BrokenEagle Date: Sun, 2 Feb 2020 06:17:15 +0000 Subject: [PATCH 3/5] Add ability to copy certain style attributes to the outer note boxes - Attributes are pulled from the first element with class "note-box-attributes" - Transform is note box only to prevent applying the same transform twice -- Only rotations are allowed to prevent excessive scaling of note boxes -- Note box positions are adjusted after drag/nudge/resize to prevent out-of-bounds -- The note body is placed on the lowest box corner that is farthest left - Background color is inner box only since the note box is already transparent -- Backgrounds with any transparency aren't allowed as they would interfere with the text below - Border radius is both since they both have borders - Add full namespaces to all event removes to prevent bad removes --- app/javascript/src/javascripts/notes.js | 176 ++++++++++++++++-- app/javascript/src/styles/specific/notes.scss | 13 +- 2 files changed, 168 insertions(+), 21 deletions(-) diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index 1afc1a60c..f0825a09d 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -4,6 +4,28 @@ import Utility from './utility' 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'], + BOX_ONLY_ATTRIBUTES: ['transform'], + INNER_ONLY_ATTRIBUTES: ['background-color'], + 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; + }, Box: { create: function(id) { @@ -56,6 +78,39 @@ let Note = { $note_box.data("height", new_height); }, + copy_style_attributes: function($note_box) { + const $note_inner_box = $note_box.find("div.note-box-inner-border"); + const $attribute_child = $note_inner_box.find('.note-box-attributes'); + let has_rotation = false; + Note.COPY_ATTRIBUTES.forEach((attribute)=>{ + const attribute_value = Note.permitted_style_values(attribute, $attribute_child); + if (Note.BOX_ONLY_ATTRIBUTES.includes(attribute)) { + $note_box.css(attribute, attribute_value); + } else if (Note.INNER_ONLY_ATTRIBUTES.includes(attribute)) { + $note_inner_box.css(attribute, attribute_value); + } else { + $note_box.css(attribute, attribute_value); + $note_inner_box.css(attribute, attribute_value); + } + if (attribute === "transform" && attribute_value.startsWith("rotate")) { + has_rotation = true; + } + }); + if (has_rotation) { + const current_left = parseFloat($note_box.css("left")); + const current_top = parseFloat($note_box.css("top")); + const position = Note.Box.get_min_max_position($note_box); + // 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.left || current_top !== position.top) { + $note_box.css(position); + $note_inner_box.addClass("out-of-bounds"); + } else { + $note_inner_box.removeClass("out-of-bounds"); + } + } + }, + bind_events: function($note_box) { $note_box.on( "dragstart.danbooru resizestart.danbooru", @@ -129,12 +184,25 @@ let Note = { } } ); + + $note_box.on("mousedown.danbooru", function(event) { + Note.drag_id = $note_box.data('id'); + }); }, find: function(id) { return $("#note-container div.note-box[data-id=" + id + "]"); }, + drag_stop: function(event) { + if (Note.drag_id !== null) { + const $note_box = Note.Box.find(Note.drag_id); + const position = Note.Box.get_min_max_position($note_box); + $note_box.css(position); + Note.drag_id = null; + } + }, + key_nudge: function(event) { if (!Note.move_id) { return; @@ -176,6 +244,9 @@ let Note = { if ($note_box.length === 0) { return; } + let computed_style = window.getComputedStyle($note_box[0]); + let current_top = parseFloat(computed_style.top); + let current_left = parseFloat(computed_style.left); let current_height = $note_box.height(); let current_width = $note_box.width(); switch (event.originalEvent.key) { @@ -195,12 +266,12 @@ let Note = { // do nothing } const position = Note.Box.get_min_max_position($note_box, null, null, current_height, current_width); - $note_box.css({ - top: position.top, - left: position.left, - height: current_height, - width: current_width, - }); + if (current_top === position.top && current_left === position.left) { + $note_box.css({ + height: current_height, + width: current_width, + }); + } Note.Box.resize_inner_border($note_box); $note_box.find(".note-box-inner-border").addClass("unsaved"); event.preventDefault(); @@ -215,14 +286,68 @@ let Note = { const $image = $("#image"); const image_height = $image.height(); const image_width = $image.width(); - current_top = Math.min(Math.max(current_top, 0), image_height - current_height - 2); - current_left = Math.min(Math.max(current_left, 0), image_width - current_width - 2); + const box_data = Note.Box.get_bounding_box($note_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); + } else { + Utility.error("Box too large to be rotated!"); + $note_box.css('transform', 'none'); + } return { - top: current_top, - left: current_left, + top: Math.round(current_top), + left: Math.round(current_left), }; }, + get_bounding_box: function($note_box, height = null, width = null) { + 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, + min_y: 0, + max_x: width, + max_y: height, + norm_coord: old_coord, + } + } + const costheta = Math.round(match[1] * 1000) / 1000; + const sintheta = Math.round(match[2] * 1000) / 1000; + let trans_x = width / 2; + let trans_y = height / 2; + let min_x = Infinity; + 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; + let rotated_x = (temp_x * costheta) - (temp_y * sintheta); + let rotated_y = (temp_x * sintheta) + (temp_y * costheta); + let new_x = rotated_x + trans_x; + let new_y = rotated_y + trans_y; + min_x = Math.min(min_x, new_x); + max_x = Math.max(max_x, new_x); + min_y = Math.min(min_y, new_y); + 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]; + }); + return { + min_x: min_x, + min_y: min_y, + max_x: max_x, + max_y: max_y, + norm_coord: norm_coord, + } + }, + show_highlighted: function($note_box) { var note_id = $note_box.data("id"); @@ -307,9 +432,12 @@ let Note = { initialize: function($note_body) { var $note_box = Note.Box.find($note_body.data("id")); + const box_data = Note.Box.get_bounding_box($note_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;}); $note_body.css({ - top: $note_box.position().top + $note_box.height() + 5, - left: $note_box.position().left + top: $note_box.position().top + selected_corner[1] + 5, + left: $note_box.position().left + selected_corner[0], }); Note.Body.bound_position($note_body); }, @@ -404,6 +532,7 @@ let Note = { $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", ""); + Note.Box.copy_style_attributes($note_box); } Note.Body.resize($note_body); Note.Body.bound_position($note_body); @@ -563,7 +692,7 @@ let Note = { 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.Box.resize_inner_border($note_box); + Note.Body.initialize($note_body); $note_body.show(); }); $this.dialog("close"); @@ -596,6 +725,7 @@ let Note = { 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(); }); }, @@ -661,7 +791,7 @@ let Note = { Note.TranslationMode.active = true; $(document.body).addClass("mode-translation"); $("#original-file-link").click(); - $("#image").off("click", Note.Box.toggle_all); + $("#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(); @@ -676,8 +806,8 @@ let Note = { Note.TranslationMode.active = false; $("#image").css("cursor", "auto"); $("#image").on("click.danbooru", Note.Box.toggle_all); - $("#image").off("mousedown", Note.TranslationMode.Drag.start); - $(document).off("mouseup", Note.TranslationMode.Drag.stop); + $("#image").off("mousedown.danbooru.note", Note.TranslationMode.Drag.start); + $(document).off("mouseup.danbooru.note", Note.TranslationMode.Drag.stop); $(document.body).removeClass("mode-translation"); $("#close-notice-link").click(); $("#mark-as-translated-section").hide(); @@ -780,7 +910,7 @@ let Note = { if (Note.TranslationMode.Drag.dragStartX === 0) { return; /* 'stop' is bound to window, don't create note if start wasn't triggered */ } - $(document).off("mousemove", Note.TranslationMode.Drag.drag); + $(document).off("mousemove.danbooru", Note.TranslationMode.Drag.drag); if (Note.TranslationMode.Drag.dragging) { $('#note-preview').css({ display: 'none' }); @@ -799,10 +929,12 @@ let Note = { id: "x", dragging: false, editing: false, - base_font_size: null, move_id: null, + drag_id: null, + base_font_size: null, timeouts: [], pending: {}, + 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); @@ -884,8 +1016,11 @@ let Note = { if (Note.embed) { Note.base_font_size = parseFloat(window.getComputedStyle($note_container[0]).fontSize); $.each($(".note-box"), function(i, note_box) { - Note.Box.resize_inner_border($(note_box)); + const $note_box = $(note_box); + Note.Box.resize_inner_border($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); }); } }, @@ -896,6 +1031,9 @@ let Note = { } Note.embed = (Utility.meta("post-has-embedded-notes") === "true"); + if (Note.embed) { + $(document).on("mouseup.danbooru.drag", Note.Box.drag_stop); + } Note.load_all(); this.initialize_shortcuts(); diff --git a/app/javascript/src/styles/specific/notes.scss b/app/javascript/src/styles/specific/notes.scss index e7e14bc05..74301309f 100644 --- a/app/javascript/src/styles/specific/notes.scss +++ b/app/javascript/src/styles/specific/notes.scss @@ -91,6 +91,13 @@ div#note-container { border: var(--unsaved-note-box-inner-border); } + &.movable { + div.note-box-inner-border, + div.note-box-inner-border.unsaved { + border: var(--movable-note-box-inner-border); + } + } + &.embedded { color: var(--note-body-text-color); border: 1px solid transparent; @@ -120,7 +127,8 @@ div#note-container { &.movable { div.note-box-inner-border, - div.note-box-inner-border.unsaved { + div.note-box-inner-border.unsaved, + div.note-box-inner-border.out-of-bounds { border: var(--movable-note-box-inner-border); } } @@ -136,7 +144,8 @@ div#note-container { border: 1px solid transparent; } - div.note-box-inner-border.unsaved { + div.note-box-inner-border.unsaved, + div.note-box-inner-border.out-of-bounds { border: var(--unsaved-note-box-inner-border); } } From 115355ab7bbc2c7ba1ca13cf7dce5fc3d27b48ae Mon Sep 17 00:00:00 2001 From: BrokenEagle Date: Fri, 14 Feb 2020 20:24:27 +0000 Subject: [PATCH 4/5] Fix dialogue text for new notes It was showing "Editing note #xx" and so on for all new notes past the first initial new note. --- app/javascript/src/javascripts/notes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index f0825a09d..0c67e6d5c 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -601,7 +601,7 @@ let Note = { } let $dialog = $('
'); - let note_title = (id === 'x' ? 'Creating new note' : 'Editing note #' + id); + let note_title = (typeof id === 'string' && id.startsWith('x') ? 'Creating new note' : 'Editing note #' + id); $dialog.append('' + note_title + ' (view help)'); $dialog.append($textarea); $dialog.data("id", id); From 2affd4a3b5a50cfb723ab27b84c22dd8412a7557 Mon Sep 17 00:00:00 2001 From: BrokenEagle Date: Fri, 14 Feb 2020 20:46:57 +0000 Subject: [PATCH 5/5] Fix post resize image control It was resizing images to the original width and height, even if they were currently being shown in their large image format (850px). This was causing excessive blur and artifacts in some cases. --- app/javascript/src/javascripts/posts.js.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/src/javascripts/posts.js.erb b/app/javascript/src/javascripts/posts.js.erb index d678c5706..9b4549713 100644 --- a/app/javascript/src/javascripts/posts.js.erb +++ b/app/javascript/src/javascripts/posts.js.erb @@ -361,6 +361,9 @@ Post.resize_image_to_window = function($img) { var sidebar_width = 0; var client_width = 0; + var isVideo = $img.prop("tagName") === "VIDEO"; + var width = isVideo ? $img.prop("width") : $img.prop('naturalWidth'); + var height = isVideo ? $img.prop("height") : $img.prop('naturalHeight'); if (($img.data("scale-factor") === 1) || ($img.data("scale-factor") === undefined)) { if ($(window).width() > 660) { sidebar_width = $("#sidebar").width() || 0; @@ -370,9 +373,6 @@ Post.resize_image_to_window = function($img) { } if ($img.width() > client_width) { - var isVideo = $img.prop("tagName") === "VIDEO"; - var width = isVideo ? $img.prop("width") : $img.data("original-width"); - var height = isVideo ? $img.prop("height") : $img.data("original-height"); var ratio = client_width / width; $img.data("scale-factor", ratio); $img.css("width", width * ratio); @@ -381,8 +381,8 @@ Post.resize_image_to_window = function($img) { } } else { $img.data("scale-factor", 1); - $img.width($img.data("original-width")); - $img.height($img.data("original-height")); + $img.width(width); + $img.height(height); Post.resize_ugoira_controls(); }