diff --git a/Gemfile.lock b/Gemfile.lock index fdb68bc26..a1443af00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,8 +77,8 @@ GEM ansi (1.5.0) ast (2.4.1) aws-eventstream (1.1.0) - aws-partitions (1.341.0) - aws-sdk-core (3.103.0) + aws-partitions (1.356.0) + aws-sdk-core (3.104.3) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) @@ -86,10 +86,10 @@ GEM aws-sdk-sqs (1.30.0) aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.1) + aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.13) - bootsnap (1.4.6) + bcrypt (3.1.15) + bootsnap (1.4.8) msgpack (~> 1.0) builder (3.2.4) byebug (11.1.3) @@ -98,7 +98,7 @@ GEM i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.0.0) + capistrano-bundler (2.0.1) capistrano (~> 3.1) capistrano-deploytags (1.0.7) capistrano (>= 3.7.0) @@ -120,13 +120,13 @@ GEM xpath (~> 3.2) childprocess (3.0.0) chronic (0.10.2) - codecov (0.2.0) + codecov (0.2.5) colorize json simplecov coderay (1.1.3) colorize (0.8.1) - concurrent-ruby (1.1.6) + concurrent-ruby (1.1.7) crass (1.0.6) daemons (1.3.1) delayed_job (4.1.8) @@ -147,7 +147,7 @@ GEM activesupport (>= 5.0.0) faraday (1.0.1) multipart-post (>= 1.2, < 3) - ffaker (2.15.0) + ffaker (2.16.0) ffi (1.13.1) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -167,7 +167,7 @@ GEM http-form_data (2.3.0) http-parser (1.2.1) ffi-compiler (>= 1.0, < 2.0) - i18n (1.8.3) + i18n (1.8.5) concurrent-ruby (~> 1.0) ipaddress_2 (0.13.0) jmespath (1.4.0) @@ -212,7 +212,7 @@ GEM net-sftp (3.0.0) net-ssh (>= 5.0.0, < 7.0.0) net-ssh (6.1.0) - newrelic_rpm (6.11.0.365) + newrelic_rpm (6.12.0.367) nio4r (2.5.2) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) @@ -244,7 +244,7 @@ GEM rack (2.2.3) rack-contrib (2.2.0) rack (~> 2.0) - rack-mini-profiler (2.0.2) + rack-mini-profiler (2.0.4) rack (>= 1.2.0) rack-proxy (0.6.5) rack @@ -293,21 +293,21 @@ GEM actionpack (>= 5.0) railties (>= 5.0) rexml (3.2.4) - rubocop (0.88.0) + rubocop (0.89.1) parallel (~> 1.10) parser (>= 2.7.1.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.7) rexml - rubocop-ast (>= 0.1.0, < 1.0) + rubocop-ast (>= 0.3.0, < 1.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.1.0) - parser (>= 2.7.0.1) - rubocop-rails (2.6.0) + rubocop-ast (0.3.0) + parser (>= 2.7.1.4) + rubocop-rails (2.7.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.82.0) + rubocop (>= 0.87.0) ruby-progressbar (1.10.1) ruby-vips (2.0.17) ffi (~> 1.9) @@ -329,7 +329,7 @@ GEM simple_form (5.0.2) actionpack (>= 5.0) activemodel (>= 5.0) - simplecov (0.18.5) + simplecov (0.19.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov-html (0.12.2) @@ -346,7 +346,7 @@ GEM stackprof (0.2.15) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - stripe (5.22.0) + stripe (5.23.1) thor (1.0.1) thread_safe (0.3.6) tzinfo (1.2.7) @@ -355,13 +355,13 @@ GEM unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - unicorn (5.5.5) + unicorn (5.6.0) kgio (~> 2.6) raindrops (~> 0.7) unicorn-worker-killer (0.4.4) get_process_mem (~> 0) unicorn (>= 4, < 6) - webpacker (5.1.1) + webpacker (5.2.0) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -373,7 +373,7 @@ GEM chronic (>= 0.6.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.3.1) + zeitwerk (2.4.0) PLATFORMS ruby diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb index 5418289a8..d9b35a46e 100644 --- a/app/controllers/emails_controller.rb +++ b/app/controllers/emails_controller.rb @@ -21,7 +21,7 @@ class EmailsController < ApplicationController end if @user.errors.none? - flash[:notice] = "Email updated" + flash[:notice] = "Email updated. Check your email to confirm your new address" UserMailer.email_change_confirmation(@user).deliver_later respond_with(@user, location: settings_url) else @@ -31,10 +31,27 @@ class EmailsController < ApplicationController end def verify - @email_address = authorize EmailAddress.find_by_user_id!(params[:user_id]) - @email_address.update!(is_verified: true) + @user = User.find(params[:user_id]) + @email_address = @user.email_address - flash[:notice] = "Email address verified" - redirect_to @email_address.user + if @email_address.blank? + redirect_to edit_user_email_path(@user) + elsif params[:email_verification_key].present? + authorize @email_address + @email_address.update!(is_verified: true) + flash[:notice] = "Email address verified" + redirect_to @email_address.user + else + authorize @email_address + respond_with(@user) + end + end + + def send_confirmation + @user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy + UserMailer.welcome_user(@user).deliver_later + + flash[:notice] = "Confirmation email sent to #{@user.email_address.address}. Check your email to confirm your address" + redirect_to @user end end diff --git a/app/controllers/moderator/post/posts_controller.rb b/app/controllers/moderator/post/posts_controller.rb index 5fac8e5a2..bc0603c16 100644 --- a/app/controllers/moderator/post/posts_controller.rb +++ b/app/controllers/moderator/post/posts_controller.rb @@ -4,18 +4,6 @@ module Moderator skip_before_action :api_check respond_to :html, :json, :xml, :js - def confirm_delete - @post = ::Post.find(params[:id]) - end - - def delete - @post = authorize ::Post.find(params[:id]) - if params[:commit] == "Delete" - @post.delete!(params[:reason], :move_favorites => params[:move_favorites].present?) - end - redirect_to(post_path(@post)) - end - def confirm_move_favorites @post = ::Post.find(params[:id]) end @@ -44,7 +32,7 @@ module Moderator def unban @post = authorize ::Post.find(params[:id]) @post.unban! - flash[:notice] = "Post was banned" + flash[:notice] = "Post was unbanned" respond_with(@post) end diff --git a/app/controllers/modqueue_controller.rb b/app/controllers/modqueue_controller.rb index 773b2cae7..ef786ec24 100644 --- a/app/controllers/modqueue_controller.rb +++ b/app/controllers/modqueue_controller.rb @@ -4,14 +4,15 @@ class ModqueueController < ApplicationController def index authorize :modqueue - @posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).pending_or_flagged.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden]) - @posts = @posts.paginated_search(params, order: "modqueue", count_pages: true) + @posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).in_modqueue.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden]) + @modqueue_posts = @posts.reselect(nil).reorder(nil).offset(nil).limit(nil) + @posts = @posts.paginated_search(params, order: "modqueue", count_pages: true, count: @modqueue_posts.to_a.size) - @modqueue_posts = @posts.except(:offset, :limit, :order) - @pending_post_count = @modqueue_posts.pending.count - @flagged_post_count = @modqueue_posts.flagged.count - @disapproval_reasons = PostDisapproval.where(post: @modqueue_posts).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id) - @uploaders = @modqueue_posts.group(:uploader).order(count: :desc).limit(20).count + @pending_post_count = @modqueue_posts.select(&:is_pending?).count + @flagged_post_count = @modqueue_posts.select(&:is_flagged?).count + @appealed_post_count = @modqueue_posts.select(&:is_appealed?).count + @disapproval_reasons = PostDisapproval.where(post_id: @modqueue_posts.map(&:id)).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id) + @uploaders = @modqueue_posts.map(&:uploader).tally.sort_by(&:last).reverse.take(20).to_h @tags = RelatedTagCalculator.frequent_tags_for_post_relation(@modqueue_posts) @artist_tags = @tags.select(&:artist?).sort_by(&:overlap_count).reverse.take(10) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 9829c954f..d5ac0e24e 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -56,6 +56,18 @@ class PostsController < ApplicationController respond_with_post_after_update(@post) end + def destroy + @post = authorize Post.find(params[:id]) + + if params[:commit] == "Delete" + move_favorites = params.dig(:post, :move_favorites).to_s.truthy? + @post.delete!(params.dig(:post, :reason), move_favorites: move_favorites, user: CurrentUser.user) + flash[:notice] = "Post deleted" + end + + respond_with_post_after_update(@post) + end + def revert @post = authorize Post.find(params[:id]) @version = @post.versions.find(params[:version_id]) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 4b00f95c2..3d7236286 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"); @@ -47,6 +43,7 @@ export { default as PostTooltip } from '../src/javascripts/post_tooltips.js'; export { default as PostVersion } from '../src/javascripts/post_version.js'; export { default as RelatedTag } from '../src/javascripts/related_tag.js'; export { default as Shortcuts } from '../src/javascripts/shortcuts.js'; +export { default as TagCounter } from '../src/javascripts/tag_counter.js'; export { default as Upload } from '../src/javascripts/uploads.js.erb'; export { default as UserTooltip } from '../src/javascripts/user_tooltips.js'; export { default as Utility } from '../src/javascripts/utility.js'; diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb index ae493b63b..a1d32f478 100644 --- a/app/javascript/src/javascripts/autocomplete.js.erb +++ b/app/javascript/src/javascripts/autocomplete.js.erb @@ -9,7 +9,7 @@ Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_ Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>; /* eslint-enable */ -Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"]; +Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated", "appealed"]; Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|"); Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|"); Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i"); diff --git a/app/javascript/src/javascripts/common.js b/app/javascript/src/javascripts/common.js index 03563a2f9..04e2af233 100644 --- a/app/javascript/src/javascripts/common.js +++ b/app/javascript/src/javascripts/common.js @@ -15,6 +15,12 @@ $(function() { e.preventDefault(); }); + $("#hide-verify-account-notice").on("click.danbooru", function(e) { + $("#verify-account-notice").hide(); + Cookie.put('hide_verify_account_notice', '1', 3); + e.preventDefault(); + }); + $("#close-notice-link").on("click.danbooru", function(e) { $('#notice').fadeOut("fast"); e.preventDefault(); diff --git a/app/javascript/src/javascripts/notes.js b/app/javascript/src/javascripts/notes.js index dac3d5cb3..05e549db2 100644 --- a/app/javascript/src/javascripts/notes.js +++ b/app/javascript/src/javascripts/notes.js @@ -1,95 +1,156 @@ -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 dragging = false; + static notes = new Set(); + static timeouts = []; + + id = null; + x = null; + y = null; + w = null; + h = 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)) { + this.note.toggle_selected(); + } 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() { + // Don't show note bodies if we mouseover another note while dragging or resizing. + if (!Note.dragging) { + this.note.body.show(); + } + } + + on_mouseleave() { + this.note.body.hide(); + } + + on_dragstart() { + this.$note_box.addClass("unsaved"); + Note.Body.hide_all(); + Note.dragging = true; + } + + // 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 rescale them and convert back + // to percentage coordinates. + on_dragstop() { + let x = this.$note_box.position().left / this.note.scale_factor; + let y = this.$note_box.position().top / this.note.scale_factor; + let w = this.$note_box.width() / this.note.scale_factor; + let h = this.$note_box.height() / this.note.scale_factor; + + this.place_note(x, y, w, h); + this.note.body.show(); + Note.dragging = false; + } + + // Place the note box. The input values are pixel coordinates relative to the full image. + 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.scale_factor; + y = position.norm_top / this.note.scale_factor; + } + + this.note.w = Math.round(clamp(w, Note.MIN_NOTE_SIZE, this.note.post_width)); + this.note.h = Math.round(clamp(h, Note.MIN_NOTE_SIZE, this.note.post_height)); + this.note.x = Math.round(clamp(x, 0, this.note.post_width - this.note.w)); + this.note.y = Math.round(clamp(y, 0, this.note.post_height - this.note.h)); + + this.$note_box.css({ + top: (100 * this.note.y / this.note.post_height) + '%', + left: (100 * this.note.x / this.note.post_width) + '%', + width: (100 * this.note.w / this.note.post_width) + '%', + height: (100 * this.note.h / this.note.post_height) + '%', + }); + } + + 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,210 +158,103 @@ 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) { switch (event.originalEvent.key) { case "ArrowUp": - current_top--; + this.note.y--; break; case "ArrowDown": - current_top++; + this.note.y++; break; case "ArrowLeft": - current_left--; + this.note.x--; break; case "ArrowRight": - current_left++; + this.note.x++; break; default: // do nothing } - let position = Note.Box.get_min_max_position($note_box, 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)); - let current_height = $note_box.height(); - let current_width = $note_box.width(); + this.place_note(this.note.x, this.note.y, this.note.w, this.note.h); + Note.Body.hide_all(); + this.$note_box.addClass("unsaved"); + event.preventDefault(); + } + + key_resize(event) { switch (event.originalEvent.key) { case "ArrowUp": - current_height--; + this.note.h--; break; case "ArrowDown": - current_height++; + this.note.h++; break; case "ArrowLeft": - current_width--; + this.note.w--; break; case "ArrowRight": - current_width++; + this.note.w++; break; default: // do nothing } - const position = Note.Box.get_min_max_position($note_box, 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) { + this.place_note(this.note.x, this.note.y, this.note.w, this.note.h); + Note.Body.hide_all(); + this.$note_box.addClass("unsaved"); + event.preventDefault(); + } + + 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 +262,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 +289,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 +298,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 +312,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 +328,64 @@ 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 $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() { + Note.Body.hide_all(); + + let large_width = parseFloat($container.data("large-width")); + let ratio = $container.width() / large_width; + let font_percentage = ratio * 100; + + $container.css('font-size', font_percentage + '%'); + } + + static toggle_all() { + Note.Body.hide_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 +396,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,92 +480,93 @@ 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.val(note.original_body); $textarea.css({ - width: "97%", height: "85%", resize: "none", }); - if (!$note_body.hasClass("new-note")) { - $textarea.val($note_body.data("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 +578,343 @@ let Note = { classes: { "ui-dialog": "note-edit-dialog", }, + open: () => { + Utility.keydown("ctrl+return", "save_note", () => this.save($dialog, note), ".note-edit-dialog textarea"); + $(".note-edit-dialog textarea").on("input.danbooru", (e) => this.on_input(note)); + $(".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 on_input(note) { + note.box.$note_box.addClass("unsaved"); + } - toggle: function(e) { - if (Note.TranslationMode.active) { - Note.TranslationMode.stop(e); + static async save($dialog, note) { + let $note_box = note.box.$note_box; + let text = $dialog.find("textarea").val(); + + let params = { + x: note.x, + y: note.y, + width: note.w, + height: note.h, + 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.body.preview_text(text); + } + + static cancel($dialog, _note) { + $dialog.dialog("close"); + } + + static async destroy($dialog, note) { + if (!note.is_new() && !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(); + Note.notes.delete(note); + + $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; - }, + Note.dragging = true; + } - 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() { + Note.dragging = false; $(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 scale_factor = $(".note-container").width() / parseInt($(".note-container").attr("data-width")); + + new Note({ + x: $("#note-preview").position().left / scale_factor, + y: $("#note-preview").position().top / scale_factor, + w: $("#note-preview").width() / scale_factor, + h: $("#note-preview").height() / scale_factor, + }); + + $("#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; + this.x = x; + this.y = y; + this.w = w; + this.h = h; - $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.add(this); + } + + is_new() { + return this.id === null; + } + + // The ratio of the current image size to the full image size. + get scale_factor() { + return this.$note_container.width() / this.post_width; + } + + // 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")); + } + + is_selected() { + return this.box.$note_box.hasClass("movable"); + } + + toggle_selected() { + return this.is_selected() ? this.unselect() : this.select(); + } + + select() { + Note.unselect_all(); + this.box.$note_box.addClass("movable"); + Utility.keydown("up down left right", "nudge_note", this.box.key_nudge.bind(this.box)); + Utility.keydown("shift+up shift+down shift+left shift+right", "resize_note", this.box.key_resize.bind(this.box)); + } + + unselect() { + this.box.$note_box.removeClass("movable"); + $(document).off("keydown.nudge_note"); + $(document).off("keydown.resize_note"); + } + + 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 Array.from(Note.notes).find(note => note.id === id); + } - load_all: function() { - var fragment = document.createDocumentFragment(); - $.each($("#notes article"), function(i, article) { + static load_all() { + $("#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"), + y: $article.data("y"), + w: $article.data("width"), + h: $article.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(); } - }, + } + + static unselect_all() { + Note.notes.forEach(note => note.unselect()); + } } $(function() { diff --git a/app/javascript/src/javascripts/post_tooltips.js b/app/javascript/src/javascripts/post_tooltips.js index 8daf42234..b4563756c 100644 --- a/app/javascript/src/javascripts/post_tooltips.js +++ b/app/javascript/src/javascripts/post_tooltips.js @@ -17,7 +17,7 @@ PostTooltip.initialize = function () { delegate("body", { allowHTML: true, - appendTo: document.body, + appendTo: document.querySelector("#post-tooltips"), delay: [PostTooltip.SHOW_DELAY, PostTooltip.HIDE_DELAY], duration: PostTooltip.DURATION, interactive: true, diff --git a/app/javascript/src/javascripts/posts.js.erb b/app/javascript/src/javascripts/posts.js.erb index 9eef63e88..db4ac4be1 100644 --- a/app/javascript/src/javascripts/posts.js.erb +++ b/app/javascript/src/javascripts/posts.js.erb @@ -14,6 +14,7 @@ Post.SWIPE_VELOCITY = 0.6; Post.MAX_RECOMMENDATIONS = 45; // 3 rows of 9 posts at 1920x1080. Post.LOW_TAG_COUNT = 10; Post.HIGH_TAG_COUNT = 20; +Post.EDIT_DIALOG_WIDTH = 720; Post.initialize_all = function() { @@ -42,8 +43,6 @@ Post.initialize_all = function() { } var $fields_multiple = $('[data-autocomplete="tag-edit"]'); - $fields_multiple.on("keypress.danbooru", Post.update_tag_count); - $fields_multiple.on("click", Post.update_tag_count); $(window).on('danbooru:initialize_saved_seraches', () => { Post.initialize_saved_searches(); @@ -105,14 +104,13 @@ Post.open_edit_dialog = function() { $("#post-edit-link").parent("li").addClass("active"); var $tag_string = $("#post_tag_string,#upload_tag_string"); - $("div.input").has($tag_string).prevAll().hide(); $("#open-edit-dialog").hide(); var dialog = $("
").attr("id", "edit-dialog"); $("#form").appendTo(dialog); dialog.dialog({ title: "Edit tags", - width: $(window).width() * 0.6, + width: Post.EDIT_DIALOG_WIDTH, position: { my: "right", at: "right-20", @@ -307,10 +305,11 @@ Post.view_original = function(e = null) { } var $image = $("#image"); + var $post = $(".image-container"); $image.attr("src", $(".image-view-original-link").attr("href")); $image.css("filter", "blur(8px)"); - $image.width($image.data("original-width")); - $image.height($image.data("original-height")); + $image.width($post.data("width")); + $image.height($post.data("height")); $image.on("load.danbooru", function() { $image.css("animation", "sharpen 0.5s forwards"); }); @@ -326,10 +325,11 @@ Post.view_large = function(e = null) { } var $image = $("#image"); + var $post = $(".image-container"); $image.attr("src", $(".image-view-large-link").attr("href")); $image.css("filter", "blur(8px)"); - $image.width($image.data("large-width")); - $image.height($image.data("large-height")); + $image.width($post.data("large-width")); + $image.height($post.data("large-height")); $image.on("load.danbooru", function() { $image.css("animation", "sharpen 0.5s forwards"); }); @@ -400,7 +400,6 @@ Post.initialize_post_sections = function() { $("#post_tag_string").focus().selectEnd().height($("#post_tag_string")[0].scrollHeight); $("#recommended").hide(); $(document).trigger("danbooru:open-post-edit-tab"); - Post.update_tag_count({target: $("#post_tag_string")}); } else if (e.target.hash === "#recommended") { $("#comments").hide(); $("#edit").hide(); @@ -511,32 +510,6 @@ Post.initialize_recommended = function() { }); }; -Post.update_tag_count = function(event) { - let string = "0 tags"; - let count = 0; - - if (event) { - let tags = Utility.regexp_split($(event.target).val()); - if (tags.length) { - count = tags.length; - string = (count === 1) ? (count + " tag") : (count + " tags") - } - } - - $("#tags-container .count").html(string); - let klass = ""; - - if (count < Post.LOW_TAG_COUNT) { - klass = "frown"; - } else if (count >= Post.LOW_TAG_COUNT && count < Post.HIGH_TAG_COUNT) { - klass = "meh"; - } else { - klass = "smile"; - } - - $("#tags-container .options #face").removeClass().addClass(`far fa-${klass}`); -} - $(document).ready(function() { Post.initialize_all(); }); diff --git a/app/javascript/src/javascripts/related_tag.js b/app/javascript/src/javascripts/related_tag.js index 3f09339ba..13dc61927 100644 --- a/app/javascript/src/javascripts/related_tag.js +++ b/app/javascript/src/javascripts/related_tag.js @@ -1,6 +1,5 @@ import Uploads from './uploads.js.erb'; import Utility from './utility'; -import Post from './posts.js.erb'; let RelatedTag = {}; @@ -121,7 +120,8 @@ RelatedTag.toggle_tag = function(e) { setTimeout(function () { $field.prop('selectionStart', $field.val().length);}, 100); e.preventDefault(); - Post.update_tag_count({ target: $field }); + // Artificially trigger input event so the tag counter updates. + $field.trigger("input"); } RelatedTag.show = function(e) { diff --git a/app/javascript/src/javascripts/tag_counter.js b/app/javascript/src/javascripts/tag_counter.js new file mode 100644 index 000000000..37ff8f46d --- /dev/null +++ b/app/javascript/src/javascripts/tag_counter.js @@ -0,0 +1,49 @@ +import { h, Component, render } from "preact"; +import { observable, computed, action } from "mobx"; +import { observer } from "mobx-react"; + +import Utility from "./utility"; + +export default @observer class TagCounter extends Component { + static lowCount = 10; + static highCount = 20; + + @observable tagCount = 0; + + componentDidMount() { + $(this.props.tags).on("input", this.updateCount); + this.updateCount(); + } + + render() { + return ( + + {this.tagCount} / {TagCounter.highCount} tags + + + ); + } + + @action.bound updateCount() { + this.tagCount = Utility.regexp_split($(this.props.tags).val()).length; + } + + @computed get iconName() { + if (this.tagCount < TagCounter.lowCount) { + return "blobglare"; + } else if (this.tagCount >= TagCounter.lowCount && this.tagCount < TagCounter.highCount) { + return "blobthinkingglare"; + } else { + return "blobaww"; + } + } + + static initialize() { + $("[data-tag-counter]").toArray().forEach(element => { + let target = $($(element).attr("data-for")).get(0); + render(h(TagCounter, { tags: target }), element); + }); + } +} + +$(TagCounter.initialize); diff --git a/app/javascript/src/javascripts/uploads.js.erb b/app/javascript/src/javascripts/uploads.js.erb index 800090537..a5ce47c5b 100644 --- a/app/javascript/src/javascripts/uploads.js.erb +++ b/app/javascript/src/javascripts/uploads.js.erb @@ -143,10 +143,13 @@ Upload.toggle_size = function(e) { Upload.update_scale = function() { let $image = $("#image"); - let natural_width = $image.get(0).naturalWidth; - let natural_height = $image.get(0).naturalHeight; - let scale_percentage = Math.round(100 * $image.width() / natural_width); - $("#upload-image-metadata-resolution").html(`(${natural_width}x${natural_height}, resized to ${scale_percentage}%)`); + + if ($image.length) { + let natural_width = $image.get(0).naturalWidth; + let natural_height = $image.get(0).naturalHeight; + let scale_percentage = Math.round(100 * $image.width() / natural_width); + $("#upload-image-metadata-resolution").html(`(${natural_width}x${natural_height}, resized to ${scale_percentage}%)`); + } } Upload.fetch_data_manual = function(e) { diff --git a/app/javascript/src/javascripts/user_tooltips.js b/app/javascript/src/javascripts/user_tooltips.js index cc0a9f3b9..eff985a8c 100644 --- a/app/javascript/src/javascripts/user_tooltips.js +++ b/app/javascript/src/javascripts/user_tooltips.js @@ -11,9 +11,9 @@ UserTooltip.DURATION = 250; UserTooltip.MAX_WIDTH = 600; UserTooltip.initialize = function () { - delegate("body", { + delegate("#page", { allowHTML: true, - appendTo: document.body, + appendTo: document.querySelector("#user-tooltips"), delay: [UserTooltip.SHOW_DELAY, UserTooltip.HIDE_DELAY], duration: UserTooltip.DURATION, interactive: true, @@ -26,7 +26,7 @@ UserTooltip.initialize = function () { onHide: UserTooltip.on_hide, }); - delegate("body", { + delegate("#user-tooltips", { allowHTML: true, interactive: true, theme: "common-tooltip", 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/base/000_vars.scss b/app/javascript/src/styles/base/000_vars.scss index af4897f49..26b84d025 100644 --- a/app/javascript/src/styles/base/000_vars.scss +++ b/app/javascript/src/styles/base/000_vars.scss @@ -1,21 +1,20 @@ -$h1_size: 2em; -$h2_size: 1.5em; -$h3_size: 1.16667em; -$h4_size: 1em; -$dtext_h1_size: 2em; -$dtext_h2_size: 1.8em; -$dtext_h3_size: 1.6em; -$dtext_h4_size: 1.4em; -$dtext_h5_size: 1.2em; -$dtext_h6_size: 1em; +:root { + --text-xs: 0.8em; + --text-sm: 0.9em; + --text-md: 1em; + --text-lg: 1.16667em; + --text-xl: 1.5em; + --text-xxl: 2em; + --header-font: Tahoma, Verdana, Helvetica, sans-serif; + --body-font: Verdana, Helvetica, sans-serif; + --monospace-font: 1.2em monospace; +} + $h1_padding: 0.8em 0 0.25em 0; $h2_padding: 0.8em 0 0.25em 0; $h3_padding: 0.8em 0 0.25em 0; $h4_padding: 0.8em 0 0.25em 0; -/* stylelint-disable-next-line value-keyword-case */ -$base_font_family: Verdana, Helvetica, sans-serif; - @mixin animated-icon { content: "►"; position: absolute; diff --git a/app/javascript/src/styles/base/020_base.scss b/app/javascript/src/styles/base/020_base.scss index 352f46349..00180c2c6 100644 --- a/app/javascript/src/styles/base/020_base.scss +++ b/app/javascript/src/styles/base/020_base.scss @@ -1,9 +1,7 @@ -@import "../base/000_vars.scss"; - body { color: var(--text-color); background-color: var(--body-background-color); - font-family: $base_font_family; + font-family: var(--body-font); font-size: 87.5%; line-height: 1.25em; } @@ -12,11 +10,6 @@ abbr[title=required] { display: none; } -code { - font-family: monospace; - font-size: 1.2em; -} - dd { margin-bottom: 1em; } @@ -25,22 +18,23 @@ dt { font-weight: bold; } -h1, h2, h3, h4, h5, h6 { - font-family: Tahoma, Verdana, Helvetica, sans-serif; +h1, h2, h3, h4, h5, h6, .heading { + font-family: var(--header-font); + font-weight: bold; line-height: 1.5em; color: var(--header-color); } h1 { - font-size: $h1_size; + font-size: var(--text-xxl); } h2 { - font-size: $h2_size; + font-size: var(--text-lg); } h3, h4, h5, h6 { - font-size: $h3_size; + font-size: var(--text-md); } fieldset { @@ -62,6 +56,11 @@ input, select, textarea { border: var(--form-input-border); color: var(--form-input-text-color); padding-left: 0.25em; + font: var(--body-font); +} + +textarea { + font-size: var(--text-sm); } input[type="button"], input[type="submit"], button { diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index 211b835fc..28eef86ec 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -104,10 +104,8 @@ --post-parent-notice-background: var(--success-background-color); --post-child-notice-background: var(--warning-background-color); --post-pending-notice-background: #D8D8FC; - --post-flagged-notice-background: var(--error-background-color); --post-banned-notice-background: var(--error-background-color); --post-deleted-notice-background: var(--error-background-color); - --post-appealed-notice-background: #D8F2FC; --post-resized-notice-background: #EED8FC; --post-search-notice-background: #EEE; @@ -145,10 +143,6 @@ --tag-count-color: var(--muted-text-color); --low-post-count-color: red; - --tag-count-indicator-frown-color: red; - --tag-count-indicator-meh-color: darkkhaki; - --tag-count-indicator-smile-color: green; - --remove-favorite-button: deeppink; --ugoira-seek-slider-background: #EEE; @@ -387,10 +381,8 @@ body[data-current-user-theme="dark"] { --post-parent-notice-background: var(--green-0); --post-resized-notice-background: var(--purple-0); --post-pending-notice-background: var(--indigo-0); - --post-flagged-notice-background: var(--red-0); --post-deleted-notice-background: var(--red-0); --post-banned-notice-background: var(--red-0); - --post-appealed-notice-background: var(--blue-0); --post-tooltip-background-color: var(--grey-3); --post-tooltip-border-color: var(--grey-4); @@ -417,10 +409,6 @@ body[data-current-user-theme="dark"] { --target-background: var(--blue-0); - --tag-count-indicator-frown-color: var(--red-1); - --tag-count-indicator-meh-color: var(--yellow-1); - --tag-count-indicator-smile-color: var(--green-1); - --uploads-dropzone-background: var(--grey-3); --uploads-dropzone-progress-bar-foreground-color: var(--link-color); --uploads-dropzone-progress-bar-background-color: var(--link-hover-color); diff --git a/app/javascript/src/styles/common/dtext.scss b/app/javascript/src/styles/common/dtext.scss index 6a9066af4..40627770d 100644 --- a/app/javascript/src/styles/common/dtext.scss +++ b/app/javascript/src/styles/common/dtext.scss @@ -9,32 +9,32 @@ div.prose { } h1 { - font-size: $dtext_h1_size; + font-size: var(--text-xl); padding: $h1_padding; } h2 { - font-size: $dtext_h2_size; + font-size: var(--text-xl); padding: $h2_padding; } h3 { - font-size: $dtext_h3_size; + font-size: var(--text-xl); padding: $h3_padding; } h4 { - font-size: $dtext_h4_size; + font-size: var(--text-xl); padding: $h4_padding; } h5 { - font-size: $dtext_h5_size; + font-size: var(--text-lg); padding: $h4_padding; } h6 { - font-size: $dtext_h6_size; + font-size: var(--text-md); padding: $h4_padding; } @@ -57,12 +57,14 @@ div.prose { list-style-type: disc; } + code, pre { + font: var(--monospace-font); + background: var(--dtext-code-background); + } + pre { - font-family: monospace; - font-size: 1.2em; margin: 0.5em 0; padding: 0.5em 1em; - background: var(--dtext-code-background); white-space: pre-wrap; } @@ -73,11 +75,6 @@ div.prose { background: var(--dtext-blockquote-background); } - code { - font-family: monospace; - background: var(--dtext-code-background); - } - .tn { font-size: 0.8em; color: var(--muted-text-color); diff --git a/app/javascript/src/styles/common/jquery_ui_custom.scss b/app/javascript/src/styles/common/jquery_ui_custom.scss index 5d28e30b2..85137ce8c 100644 --- a/app/javascript/src/styles/common/jquery_ui_custom.scss +++ b/app/javascript/src/styles/common/jquery_ui_custom.scss @@ -1,10 +1,8 @@ -@import "../base/000_vars.scss"; - .ui-widget { - font-family: $base_font_family; + font-family: var(--body-font); input, select, textarea, button { - font-family: $base_font_family; + font-family: var(--body-font); } } diff --git a/app/javascript/src/styles/common/main_layout.scss b/app/javascript/src/styles/common/main_layout.scss index 1af5253b4..31311a6a2 100644 --- a/app/javascript/src/styles/common/main_layout.scss +++ b/app/javascript/src/styles/common/main_layout.scss @@ -6,10 +6,6 @@ div#page { padding: 0 10px; aside#sidebar { - h1 { - font-size: $h3_size; - } - #options-box i.fa-bookmark { margin-right: 0.25em; } diff --git a/app/javascript/src/styles/common/messages.scss b/app/javascript/src/styles/common/messages.scss index 4d70c0103..b38857027 100644 --- a/app/javascript/src/styles/common/messages.scss +++ b/app/javascript/src/styles/common/messages.scss @@ -15,6 +15,10 @@ div.list-of-messages { width: 12em; margin-right: 1em; + div.author-name { + font-weight: bold; + } + a.message-timestamp { font-style: italic; font-size: 0.90em; @@ -42,7 +46,7 @@ div.list-of-messages { margin: 0 0 1em; width: auto; - h4 { + div.author-name { display: inline; margin-right: 0.5em; } diff --git a/app/javascript/src/styles/common/page_header.scss b/app/javascript/src/styles/common/page_header.scss index ea89e5394..11301b97e 100644 --- a/app/javascript/src/styles/common/page_header.scss +++ b/app/javascript/src/styles/common/page_header.scss @@ -3,8 +3,8 @@ } header#top { - h1#app-name-header { - font-size: 2em; + #app-name-header { + font-size: var(--text-xxl); margin: 0 30px; } diff --git a/app/javascript/src/styles/common/simple_form.scss b/app/javascript/src/styles/common/simple_form.scss index 2b1706df4..d0304dc44 100644 --- a/app/javascript/src/styles/common/simple_form.scss +++ b/app/javascript/src/styles/common/simple_form.scss @@ -48,7 +48,6 @@ form.simple_form { textarea { width: 70%; - font-size: 1.2em; } label { @@ -107,10 +106,31 @@ form.one-line-form { } div.ui-dialog { - div.input { - input[type="text"] { - width: 100%; - max-width: 100%; + textarea, input[type="text"] { + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + + form.simple_form { + margin-bottom: 0; + + div.input { + margin-bottom: 0.5em; + } + + div.input.hidden { + display: none; + } + } + + /* the submit and close buttons */ + .ui-dialog-buttonpane { + margin-top: 0; + padding: 1em 1em 1em 0; + + .ui-button { + margin: 0 0.25em; } } } diff --git a/app/javascript/src/styles/specific/error.scss b/app/javascript/src/styles/specific/error.scss index cbd2288fc..468a2c41e 100644 --- a/app/javascript/src/styles/specific/error.scss +++ b/app/javascript/src/styles/specific/error.scss @@ -1,6 +1,5 @@ ul.backtrace { - font-family: monospace; - font-size: 1.2em; + font: var(--monospace-font); background: var(--dtext-code-background); padding: 1em; margin-bottom: 1em; diff --git a/app/javascript/src/styles/specific/notes.scss b/app/javascript/src/styles/specific/notes.scss index ba02223ad..96f32264c 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; @@ -93,6 +95,11 @@ opacity: 0.5; z-index: 100; + /* Raise notes on hover so overlapping embedded notes are readable. */ + &:hover { + z-index: 200; + } + &.unsaved { border: var(--unsaved-note-box-border); } @@ -106,7 +113,7 @@ border: 1px solid transparent; opacity: 1; - &.hovering { + &:hover { border: var(--note-box-border); box-shadow: var(--note-box-shadow); @@ -114,10 +121,6 @@ &.movable { opacity: 1; } - - div.ui-resizable-handle { - display: block; - } } &.editing, @@ -134,24 +137,30 @@ border: var(--movable-note-box-border); } - div.ui-resizable-handle { - display: none; + &:not(:hover) 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; background: var(--note-preview-background); - z-index: 100; + z-index: 250; } div.note-edit-dialog { diff --git a/app/javascript/src/styles/specific/pools.scss b/app/javascript/src/styles/specific/pools.scss index 9ef7f703f..93d90cc3f 100644 --- a/app/javascript/src/styles/specific/pools.scss +++ b/app/javascript/src/styles/specific/pools.scss @@ -27,17 +27,9 @@ div#add-to-pool-dialog { margin-left: 1em; cursor: pointer; } - - h1 { - font-size: $h3_size; - } } div#c-pools { - h1 { - font-size: $h2_size; - } - textarea { height: 10em; } @@ -50,10 +42,6 @@ div#c-pools { } div#c-pool-orders, div#c-favorite-group-orders { - h1 { - font-size: $h2_size; - } - div#a-edit { ul.ui-sortable { list-style-type: none; diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index 03a8eb93c..1eca2209a 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -80,8 +80,13 @@ table article.post-preview { margin-top: 1em; } -#edit-dialog textarea { - margin-bottom: 0.25em; +#edit-dialog { + /* Hide everything but the rating and tags fields. */ + .post_has_embedded_notes_fieldset, .post_lock_fieldset, .post_parent_id, + .post_source, #filedropzone, .upload_as_pending, .upload_source_container, + .upload_parent_id, .upload_artist_commentary_container, .upload_commentary_translation_container { + display: none; + } } .post-preview { @@ -258,26 +263,15 @@ div#c-posts { margin-bottom: 0; } - .resolved { - margin-left: 0.5em; - font-weight: bold; - } - &.post-notice-parent { background: var(--post-parent-notice-background); } &.post-notice-child { background: var(--post-child-notice-background); } &.post-notice-pending { background: var(--post-pending-notice-background); } - &.post-notice-flagged { background: var(--post-flagged-notice-background); } &.post-notice-banned { background: var(--post-banned-notice-background); } &.post-notice-deleted { background: var(--post-deleted-notice-background); } - &.post-notice-appealed { background: var(--post-appealed-notice-background); } &.post-notice-resized { background: var(--post-resized-notice-background); } &.post-notice-search { background: var(--post-search-notice-background); } } - aside#sidebar #tag-list h2 { - font-size: $h4_size; - } - aside#sidebar > section > ul { margin-bottom: 1em; @@ -308,7 +302,7 @@ div#c-posts { div#a-index { menu#post-sections { margin-bottom: 0.5em; - font-size: $h3_size; + font-size: var(--text-lg); li { padding: 0 1em 0.5em 0; @@ -331,7 +325,7 @@ div#c-posts { menu#post-sections { margin: 0; - font-size: $h3_size; + font-size: var(--text-lg); li { padding: 0 1em 0 0; @@ -434,12 +428,27 @@ div#c-posts { } } -body[data-post-current-image-size="large"] .image-view-large-link, -body[data-post-current-image-size="original"] .image-view-original-link, +body[data-post-current-image-size="large"] #post-options .image-view-large-link, +body[data-post-current-image-size="original"] #post-options .image-view-original-link, body[data-post-current-image-size="original"] #image-resize-notice { display: none; } +/* Always show the "Resized to X% of original" notice on mobile when it exists. */ +#image-resize-notice { + @media screen and (max-width: 660px) { + display: block !important; + } +} + +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 { @@ -474,6 +483,33 @@ div#c-posts, div#c-uploads { } } +/* Container for the tag edit