From e5d0accc15a5854e47f9233ce61809b9ecbddc83 Mon Sep 17 00:00:00 2001 From: Chad Date: Thu, 11 May 2023 17:36:50 -0700 Subject: [PATCH 1/5] Add dialog to screenshot a proposal --- @client/item.coffee | 149 +++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/@client/item.coffee b/@client/item.coffee index 74dd4c92f..6a0d9fa39 100644 --- a/@client/item.coffee +++ b/@client/item.coffee @@ -16,6 +16,7 @@ window.PROPOSAL_DESCRIPTION_MAX_HEIGHT_ON_EXPAND = 500 require './item_text' require './item_opinion' require './histogram_scores' +htmlToImage = require 'html-to-image' @@ -590,8 +591,43 @@ styles += """ } -""" + .ProposalItem .screenshot-menu-button { + display:none; + } + .ProposalItem.is_expanded .screenshot-menu-button { + display:block; + } + .ProposalItem .screenshot-menu { + display:none; + } + .ProposalItem.is_expanded .screenshot-menu { + display:block; + } + .ProposalBlock .screenshot-menu { + border: solid 1px black; + background-color: white; + padding: 20px; + position: absolute; + z-index: 100; + box-shadow: 5px 5px 20px #888888; + } + .ProposalBlock .screenshot-menu-close { + position: absolute; + top: 5px; + right: 5px; + cursor: pointer; + } + .ProposalBlock .screenshot-image { + border: solid 1px #888888; + } + .ProposalBlock .screenshot-include { + margin: 5px; + } + .ProposalBlock .screenshot-button { + margin: 10px; + } +""" @@ -759,6 +795,117 @@ ProposalBlock = ReactiveComponent + BUTTON + className: 'like_link screenshot-menu-button' + onClick: @toggleScreenshotMenu + import_icon 20, 20, '#222' + DIV + className: 'screenshot-menu' + style: + display: 'none' + DIV + className: 'screenshot-menu-close' + onClick: @toggleScreenshotMenu + 'X' + DIV + className: 'screenshot-image' + 'screenshot preview' + DIV + className: 'screenshot-include' + LABEL + htmlFor: 'include' + 'Include: ' + SELECT + id: 'include' + className: 'screenshot-include-select' + onChange: @updateScreenshot + + for option in ['opinions and reasons', 'opinions', 'reasons'] + OPTION + key: option + value: option + option + BUTTON + className: 'screenshot-button' + onClick: (e) -> + proposalDiv = e.target.closest( '.proposal-block-container' ) + previewDiv = proposalDiv.querySelector('.screenshot-image') + dataUrlState = fetch 'screenshot' + dataUrl = if dataUrlState then dataUrlState['data'] else null + console.info( 'dataUrl=', dataUrl ) + if dataUrl and navigator.clipboard + navigator.clipboard.write( [ new ClipboardItem({'image/png': dataUrl}) ] ) + alert( 'Copied screenshot to clipboard' ) + else + alert( 'Screenshot copy failed for dataUrl=' + dataUrl ) + 'Copy screenshot' + BUTTON + className: 'screenshot-button' + onClick: (e) -> + proposalItem = e.target.closest( '.ProposalItem' ) + console.info( 'proposalItem=', proposalItem ) + proposalId = proposalItem.id.substring(1).replace('_', '-') + console.info( 'proposalId=', proposalId ) + pageUrl = new URL( document.location ) + proposalUrl = pageUrl.protocol + '//' + pageUrl.host + '/' + proposalId + console.info( 'proposalUrl=', proposalUrl ) + if proposalUrl and navigator.clipboard + navigator.clipboard.writeText( proposalUrl ) + alert( 'Copied URL to clipboard: ' + proposalUrl ) + else + alert( 'Screenshot copy failed for proposalUrl=' + proposalUrl ) + 'Copy link' + + + toggleScreenshotMenu: (e) -> + proposalDiv = e.target.closest( '.proposal-block-container' ) + console.info( 'toggleScreenshotMenu() proposalDiv=', proposalDiv ) + screenshotMenu = proposalDiv.querySelector('.screenshot-menu') + console.info( 'toggleScreenshotMenu() screenshotMenu=', screenshotMenu ) + screenshotMenu.style.display = if screenshotMenu.style.display then null else 'none' + if screenshotMenu.style.display != 'none' + @updateScreenshot(e) + + + updateScreenshot: (e) -> + console.info( 'updateScreenshot e=', e ) + + proposalDiv = e.target.closest( '.proposal-block-container' ) + reasonsDiv = proposalDiv.querySelector('section.reasons_region') + includeSelect = proposalDiv.querySelector('.screenshot-include-select') + + opinionsAndReasonsDiv = proposalDiv.querySelector('.OpinionBlock') + opinionsDiv = proposalDiv.querySelector('.fast-thought') + reasonsDiv = proposalDiv.querySelector('.slow-thought') + targetDiv = opinionsAndReasonsDiv + + if includeSelect.value == 'reasons' + targetDiv = reasonsDiv + if includeSelect.value == 'opinions' + targetDiv = opinionsDiv + + htmlToImage.toPng( targetDiv ).then( (dataUrl) => + targetBounds = targetDiv.getBoundingClientRect() + image = new Image( targetBounds.width, targetBounds.height ) + image.src = dataUrl + image.style.width = '500px' + image.style.height = 'auto' + console.warn( 'image=', image ) + + previewDiv = proposalDiv.querySelector('.screenshot-image') + previewDiv.replaceChild( image, previewDiv.firstChild ) + + dataUrlState = fetch 'screenshot' + dataUrlState['data'] = dataUrl + save dataUrlState + ).catch( (err) => + console.warn('toPng err=', err) + ) + + + + + styles += """ .edit_and_delete_block { opacity: 0; diff --git a/package.json b/package.json index 952cfacb6..e592df939 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "yamljs": "^0.2.1", "webpack": "1.11.0", "chunk-manifest-webpack-plugin": "0.0.1", - "compression-webpack-plugin": "0.2.0" + "compression-webpack-plugin": "0.2.0", + "html-to-image": "1.11.11" } } From 2b32272399eb5017c3a53f85d774a8d0a6b254fa Mon Sep 17 00:00:00 2001 From: chadbrower0 Date: Thu, 25 May 2023 13:43:05 -0700 Subject: [PATCH 2/5] Screenshot background color --- @client/item.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@client/item.coffee b/@client/item.coffee index 6a0d9fa39..4360708cb 100644 --- a/@client/item.coffee +++ b/@client/item.coffee @@ -884,7 +884,7 @@ ProposalBlock = ReactiveComponent if includeSelect.value == 'opinions' targetDiv = opinionsDiv - htmlToImage.toPng( targetDiv ).then( (dataUrl) => + htmlToImage.toPng( targetDiv, {backgroundColor:'#ffffff'} ).then( (dataUrl) => targetBounds = targetDiv.getBoundingClientRect() image = new Image( targetBounds.width, targetBounds.height ) image.src = dataUrl From ee923a5ddcaf357c6dcd03a54b90d4c630fc0328 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 20 Jun 2023 20:58:15 -0700 Subject: [PATCH 3/5] Filter screenshot contents more --- @client/item.coffee | 124 ++++++++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/@client/item.coffee b/@client/item.coffee index 6a0d9fa39..39b0aa75f 100644 --- a/@client/item.coffee +++ b/@client/item.coffee @@ -592,16 +592,17 @@ styles += """ .ProposalItem .screenshot-menu-button { - display:none; + display: none; } .ProposalItem.is_expanded .screenshot-menu-button { - display:block; + display: block; } .ProposalItem .screenshot-menu { - display:none; + display: none; + min-width: 400px; } .ProposalItem.is_expanded .screenshot-menu { - display:block; + display: block; } .ProposalBlock .screenshot-menu { border: solid 1px black; @@ -809,27 +810,52 @@ ProposalBlock = ReactiveComponent 'X' DIV className: 'screenshot-image' - 'screenshot preview' + 'Screenshot not available' + DIV + className: 'screenshot-include' + LABEL + htmlFor: 'includeTitle' + 'Include title' + INPUT + type: 'checkbox' + id: 'includeTitle' + defaultChecked: true + onChange: @updateScreenshot + DIV + className: 'screenshot-include' + LABEL + htmlFor: 'includeMetadata' + 'Include time & counts' + INPUT + type: 'checkbox' + id: 'includeMetadata' + defaultChecked: false + onChange: @updateScreenshot DIV className: 'screenshot-include' LABEL - htmlFor: 'include' - 'Include: ' - SELECT - id: 'include' - className: 'screenshot-include-select' + htmlFor: 'includeOpinions' + 'Include opinions' + INPUT + type: 'checkbox' + id: 'includeOpinions' + defaultChecked: true + onChange: @updateScreenshot + DIV + className: 'screenshot-include' + LABEL + htmlFor: 'includeReasons' + 'Include pros & cons' + INPUT + type: 'checkbox' + id: 'includeReasons' + defaultChecked: true onChange: @updateScreenshot - for option in ['opinions and reasons', 'opinions', 'reasons'] - OPTION - key: option - value: option - option BUTTON className: 'screenshot-button' onClick: (e) -> proposalDiv = e.target.closest( '.proposal-block-container' ) - previewDiv = proposalDiv.querySelector('.screenshot-image') dataUrlState = fetch 'screenshot' dataUrl = if dataUrlState then dataUrlState['data'] else null console.info( 'dataUrl=', dataUrl ) @@ -853,7 +879,7 @@ ProposalBlock = ReactiveComponent navigator.clipboard.writeText( proposalUrl ) alert( 'Copied URL to clipboard: ' + proposalUrl ) else - alert( 'Screenshot copy failed for proposalUrl=' + proposalUrl ) + alert( 'Link copy failed for proposalUrl=' + proposalUrl ) 'Copy link' @@ -868,29 +894,57 @@ ProposalBlock = ReactiveComponent updateScreenshot: (e) -> - console.info( 'updateScreenshot e=', e ) - + # Use CSS to hide some elements during screenshot, removing their space as well. + # Directly access proposal-element class because we do not want to save screenshot-state to proposal on server, + # we and cannot push it up to proposal-display through @local. proposalDiv = e.target.closest( '.proposal-block-container' ) - reasonsDiv = proposalDiv.querySelector('section.reasons_region') - includeSelect = proposalDiv.querySelector('.screenshot-include-select') + proposalDiv.classList.add( 'screenshot_in_progress' ) - opinionsAndReasonsDiv = proposalDiv.querySelector('.OpinionBlock') - opinionsDiv = proposalDiv.querySelector('.fast-thought') + reasonsDiv = proposalDiv.querySelector('section.reasons_region') + includeTitleCheckbox = proposalDiv.querySelector('#includeTitle') + includeMetadataCheckbox = proposalDiv.querySelector('#includeMetadata') + includeOpinionsCheckbox = proposalDiv.querySelector('#includeOpinions') + includeReasonsCheckbox = proposalDiv.querySelector('#includeReasons') + + # Use javascript to hide elements based on selected checkboxes. + # Could screenshot each part with toCanvas(), then merge canvases -- but parts would be poorly aligned. + opinionsDiv = proposalDiv.querySelector('.Slidergram') + opinionsDiv.style.display = if includeOpinionsCheckbox.checked then null else 'none' reasonsDiv = proposalDiv.querySelector('.slow-thought') - targetDiv = opinionsAndReasonsDiv + reasonsDiv.style.display = if includeReasonsCheckbox.checked then null else 'none' + titleDiv = proposalDiv.querySelector('.proposal-title-text') + titleDiv.style.display = if includeTitleCheckbox.checked then null else 'none' + metadataDiv = proposalDiv.querySelector('.proposal-metadata') + metadataDiv.style.display = if includeMetadataCheckbox.checked then null else 'none' + targetDiv = proposalDiv + + # Use htmlToImage filter-callback to exclude some elements, but keep their space. + excludeClasses = [ 'avatar', 'proposal-avatar-wrapper', 'opinion-views-container', 'opinion-heading', 'DecisionBoard', \ + 'proposal-description-wrapper', 'bottom_closer', 'edit_and_delete_block' ] + if not includeOpinionsCheckbox.checked + excludeClasses.push( 'Slidergram' ) + if not includeReasonsCheckbox.checked + excludeClasses.push( 'slow-thought' ) + if not includeTitleCheckbox.checked + excludeClasses.push( 'proposal-title-text' ) + if not includeMetadataCheckbox.checked + excludeClasses.push( 'proposal-metadata' ) + filter = ( htmlElement ) -> + elementHasClass = ( className ) -> htmlElement?.classList?.contains( className ) + not excludeClasses.some( elementHasClass ) + + htmlToImage.toPng( targetDiv, {backgroundColor:'#ffffff', filter:filter} ).then( (dataUrl) => + proposalDiv.classList.remove( 'screenshot_in_progress' ) + opinionsDiv.style.display = null + reasonsDiv.style.display = null + titleDiv.style.display = null + metadataDiv.style.display = null - if includeSelect.value == 'reasons' - targetDiv = reasonsDiv - if includeSelect.value == 'opinions' - targetDiv = opinionsDiv - - htmlToImage.toPng( targetDiv ).then( (dataUrl) => targetBounds = targetDiv.getBoundingClientRect() image = new Image( targetBounds.width, targetBounds.height ) image.src = dataUrl image.style.width = '500px' image.style.height = 'auto' - console.warn( 'image=', image ) previewDiv = proposalDiv.querySelector('.screenshot-image') previewDiv.replaceChild( image, previewDiv.firstChild ) @@ -898,8 +952,14 @@ ProposalBlock = ReactiveComponent dataUrlState = fetch 'screenshot' dataUrlState['data'] = dataUrl save dataUrlState + ).catch( (err) => - console.warn('toPng err=', err) + console.warn('htmlToImage.toPng() err=', err) + proposalDiv.classList.remove( 'screenshot_in_progress' ) + opinionsDiv.style.display = null + reasonsDiv.style.display = null + titleDiv.style.display = null + metadataDiv.style.display = null ) From 52e7db03f5d758a8bff02df6c42e935bfc9627cb Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 21 Jun 2023 09:45:05 -0700 Subject: [PATCH 4/5] Style screenshot dialog more consistent with other popups --- @client/item.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/@client/item.coffee b/@client/item.coffee index 39b0aa75f..03c3aec79 100644 --- a/@client/item.coffee +++ b/@client/item.coffee @@ -606,7 +606,8 @@ styles += """ } .ProposalBlock .screenshot-menu { border: solid 1px black; - background-color: white; + border-radius: 8px; + background-color: #eeeeee; padding: 20px; position: absolute; z-index: 100; @@ -626,6 +627,7 @@ styles += """ } .ProposalBlock .screenshot-button { margin: 10px; + opacity: 1.0; } """ From 2f9553093bd9a6dde3a244d4f6e1dcab82224373 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 21 Jun 2023 09:53:22 -0700 Subject: [PATCH 5/5] Style screenshot button more consistently with other buttons --- @client/item.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/@client/item.coffee b/@client/item.coffee index 03c3aec79..ad7cbd41c 100644 --- a/@client/item.coffee +++ b/@client/item.coffee @@ -626,8 +626,14 @@ styles += """ margin: 5px; } .ProposalBlock .screenshot-button { - margin: 10px; opacity: 1.0; + margin: 10px; + border-radius: 8px; + border: none; + padding: 5px; + background-color: #456ae4; + color: white; + font-weight: bold; } """