deep-linking-sample/Apps/Wikipedia/Wikipedia/Code/ArticleWebMessagingController.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

716 lines
31 KiB
Swift

import Foundation
import CocoaLumberjackSwift
protocol ArticleWebMessageHandling: AnyObject {
func didRecieve(action: ArticleWebMessagingController.Action)
}
class ArticleWebMessagingController: NSObject {
fileprivate weak var webView: WKWebView?
weak var delegate: ArticleWebMessageHandling?
private let bodyActionKey = "action"
private let bodyDataKey = "data"
var parameters: PageContentService.Setup.Parameters?
var contentController: WKUserContentController?
var shouldAttemptToShowArticleAsLivingDoc = false
func setup(with webView: WKWebView, languageCode: String, theme: Theme, layoutMargins: UIEdgeInsets, leadImageHeight: CGFloat = 0, areTablesInitiallyExpanded: Bool = false, textSizeAdjustment: Int? = nil, userGroups: [String] = []) {
let margins = getPageContentServiceMargins(from: layoutMargins)
let textSizeAdjustment = textSizeAdjustment ?? UserDefaults.standard.wmf_articleFontSizeMultiplier() as? Int ?? 100
let parameters = PageContentService.Setup.Parameters(theme: theme.webName.lowercased(), dimImages: theme.imageOpacity < 1, margins: margins, leadImageHeight: "\(leadImageHeight)px", areTablesInitiallyExpanded: areTablesInitiallyExpanded, textSizeAdjustmentPercentage: "\(textSizeAdjustment)%", userGroups: userGroups)
self.parameters = parameters
self.webView = webView
let contentController = webView.configuration.userContentController
contentController.add(self, name: PageContentService.messageHandlerName)
self.contentController = contentController
do {
try updateUserScripts(on: contentController, with: parameters)
} catch let error {
WMFAlertManager.sharedInstance.showErrorAlert(error as NSError, sticky: true, dismissPreviousAlerts: false)
}
}
func removeScriptMessageHandler() {
webView?.configuration.userContentController.removeScriptMessageHandler(forName: PageContentService.messageHandlerName)
}
/// Update the scripts that run on page load. Utilize this when any parameters change.
func updateUserScripts(on contentController: WKUserContentController, with parameters: PageContentService.Setup.Parameters) throws {
let pcsSetup = try PageContentService.SetupScript(parameters)
// Unfortunately we can only remove all and re-add all
// it doesn't appear there's any way to just replace the script we need to update
contentController.removeAllUserScripts()
contentController.addUserScript(pcsSetup)
let propertiesScript = PageContentService.PropertiesScript()
contentController.addUserScript(propertiesScript)
let utilitiesScript = PageContentService.UtilitiesScript()
contentController.addUserScript(utilitiesScript)
if shouldAttemptToShowArticleAsLivingDoc {
let significantEventsStyleScript = PageContentService.SignificantEventsStyleScript(theme: parameters.theme)
contentController.addUserScript(significantEventsStyleScript)
} else {
let styleScript = PageContentService.StyleScript()
contentController.addUserScript(styleScript)
}
}
func addFooter(articleURL: URL, restAPIBaseURL: URL, menuItems: [PageContentService.Footer.Menu.Item], lastModified: Date?) {
guard let title = articleURL.percentEncodedPageTitleForPathComponents else {
return
}
var editedDaysAgo: Int?
if let lastModified = lastModified {
editedDaysAgo = NSCalendar.wmf_gregorian().wmf_days(from: lastModified, to: Date())
}
let menu = PageContentService.Footer.Menu(items: menuItems, editedDaysAgo: editedDaysAgo)
let readMore = PageContentService.Footer.ReadMore(itemCount: 3, baseURL: restAPIBaseURL.absoluteString)
let parameters = PageContentService.Footer.Parameters(title: title, menu: menu, readMore: readMore)
guard let parametersJS = try? PageContentService.getJavascriptFor(parameters) else {
return
}
webView?.evaluateJavaScript("pcs.c1.Footer.add(\(parametersJS))", completionHandler: { (result, error) in
if let error = error {
DDLogError("Error adding footer: \(error)")
}
})
}
// MARK: - Adjustable state
// MARK: PCS
/// Update the pageSetupSettings so that they are correct on reload
/// Without updating these after changing settings like theme, text size, etc, the page will revert to the original settings on reload
func updateSetupParameters() {
guard let parameters = parameters, let contentController = contentController else {
return
}
try? updateUserScripts(on: contentController, with: parameters)
}
func updateTheme(_ theme: Theme) {
let webTheme = theme.webName.lowercased()
let js = "pcs.c1.Page.setTheme(pcs.c1.Themes.\(theme.webName.uppercased()))"
webView?.evaluateJavaScript(js)
parameters?.theme = webTheme
updateSetupParameters()
if shouldAttemptToShowArticleAsLivingDoc {
let significantEventsJS = PageContentService.SignificantEventsStyleScript.sourceForTheme(webTheme)
webView?.evaluateJavaScript(significantEventsJS)
}
}
func getPageContentServiceMargins(from insets: UIEdgeInsets, leadImageHeight: CGFloat = 0) -> PageContentService.Setup.Parameters.Margins {
return PageContentService.Setup.Parameters.Margins(top: "\(insets.top + leadImageHeight)px", right: "\(insets.right)px", bottom: "\(insets.bottom)px", left: "\(insets.left)px")
}
func updateMargins(with layoutMargins: UIEdgeInsets, leadImageHeight: CGFloat) {
let margins = getPageContentServiceMargins(from: layoutMargins, leadImageHeight: leadImageHeight)
guard let marginsJSON = try? PageContentService.getJavascriptFor(margins) else {
return
}
webView?.evaluateJavaScript("pcs.c1.Page.setMargins(\(marginsJSON))")
parameters?.margins = margins
updateSetupParameters()
}
func updateTextSizeAdjustmentPercentage(_ percentage: Int) {
let percentage = "\(percentage)%"
let js = "pcs.c1.Page.setTextSizeAdjustmentPercentage('\(percentage)')"
webView?.evaluateJavaScript(js)
parameters?.textSizeAdjustmentPercentage = percentage
updateSetupParameters()
}
func prepareForScroll(to anchor: String, highlight: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
guard let webView = webView else {
completion(.failure(RequestError.invalidParameters))
return
}
webView.evaluateJavaScript("pcs.c1.Page.prepareForScrollToAnchor(`\(anchor.sanitizedForJavaScriptTemplateLiterals)`, {highlight: \(highlight ? "true" : "false")})") { (result, error) in
if let error = error {
DDLogError("Error attempting to scroll to anchor: \(anchor) \(error)")
completion(.failure(error))
} else {
completion(.success(()))
}
}
}
func removeElementHighlights() {
webView?.evaluateJavaScript("pcs.c1.Page.removeHighlightsFromHighlightedElements()")
}
// MARK: iOS App Specific overrides (code in www/, built products in assets/)
func removeSearchTermHighlights() {
let js = "window.wmf.findInPage.removeSearchTermHighlights()"
webView?.evaluateJavaScript(js)
}
}
struct ReferenceBackLink {
let id: String
init?(scriptMessageDict: [String: Any]) {
guard
let id = scriptMessageDict["id"] as? String
else {
return nil
}
self.id = id
}
}
extension ArticleWebMessagingController: WKScriptMessageHandler {
/// Actions represent events from the web page
enum Action {
case setup
case finalSetup
case image(src: String, href: String, width: Int?, height: Int?)
case link(href: String, text: String?, title: String?)
case reference(selectedIndex: Int, group: [WMFLegacyReference])
case backLink(referenceId: String, referenceText: String, backLinks: [ReferenceBackLink])
case edit(sectionID: Int, descriptionSource: ArticleDescriptionSource?)
case addTitleDescription
case footerItem(type: PageContentService.Footer.Menu.Item, payload: Any?)
case viewInBrowser
case leadImage(source: String?, width: Int?, height: Int?)
case tableOfContents(items: [TableOfContentsItem])
case scrollToAnchor(anchor: String, rect: CGRect)
case aaaldInsertOnScreen
case unknown(href: String)
}
// PCSActions are receieved from the JS bridge and converted into actions
// Handle both _clicked and non-clicked variants in case the names change
private enum PCSAction: String {
case setup
case finalSetup = "final_setup"
case image
case link
case reference
case backLink = "back_link"
case pronunciation
case edit = "edit_section"
case addTitleDescription = "add_title_description"
case footerItem = "footer_item"
case viewInBrowser = "view_in_browser"
case leadImage
case tableOfContents
case scrollToAnchor = "scroll_to_anchor"
case aaaldInsertOnScreen
init?(pcsActionString: String) {
let cleaned = pcsActionString.replacingOccurrences(of: "_clicked", with: "")
self.init(rawValue: cleaned)
}
func getAction(with data: [String: Any]?) -> Action? {
switch self {
case .setup:
return .setup
case .finalSetup:
return .finalSetup
case .image:
return getImageAction(with: data)
case .reference:
return getReferenceAction(with: data)
case .backLink:
return getBackLinkAction(with: data)
case .pronunciation:
return getPronunciationAction(with: data)
case .edit:
return getEditAction(with: data)
case .addTitleDescription:
return .addTitleDescription
case .footerItem:
return getFooterItemAction(with: data)
case .viewInBrowser:
return .viewInBrowser
case .link:
return getLinkAction(with: data)
case .leadImage:
return getLeadImageAction(with: data)
case .tableOfContents:
return getTableOfContentsAction(with: data)
case .scrollToAnchor:
return getScrollToAnchorAction(with: data)
case .aaaldInsertOnScreen:
return .aaaldInsertOnScreen
}
}
func getLeadImageAction(with data: [String: Any]?) -> Action? {
// Send back a lead image event even if it's empty - we need to handle this case
let leadImage = data?["leadImage"] as? [String: Any]
let source = leadImage?["source"] as? String
let width = leadImage?["width"] as? Int
let height = leadImage?["height"] as? Int
return Action.leadImage(source: source, width: width, height: height)
}
func getTableOfContentsAction(with data: [String: Any]?) -> Action? {
guard let tableOfContents = data?["tableOfContents"] as? [[String: Any]] else {
return nil
}
var currentRootSectionId = -1
let items = tableOfContents.compactMap { (tocJSON) -> TableOfContentsItem? in
guard
let id = tocJSON["id"] as? Int,
let level = tocJSON["level"] as? Int,
let anchor = tocJSON["anchor"] as? String,
let title = tocJSON["title"] as? String
else {
return nil
}
let indentationLevel = level - 1
if indentationLevel == 0 {
currentRootSectionId = id
}
return TableOfContentsItem(id: id, titleHTML: title, anchor: anchor, rootItemId: currentRootSectionId, indentationLevel: indentationLevel)
}
return Action.tableOfContents(items: items)
}
func getLinkAction(with data: [String: Any]?) -> Action? {
guard let href = data?["href"] as? String else {
return nil
}
let title = data?["title"] as? String
let text = data?["text"] as? String
return .link(href: href, text: text, title: title)
}
func getEditAction(with data: [String: Any]?) -> Action? {
guard let sectionIDString = data?["sectionId"] as? String, let sectionID = Int(sectionIDString) else {
return nil
}
let sourceString = data?["descriptionSource"] as? String
let source = ArticleDescriptionSource.from(string: sourceString)
return .edit(sectionID: sectionID, descriptionSource: source)
}
func getImageAction(with data: [String: Any]?) -> Action? {
guard let src = data?["src"] as? String, let href = data?["href"] as? String else {
return nil
}
let width = data?["data-file-width"] as? Int
let height = data?["data-file-height"] as? Int
return .image(src: src, href: href, width: width, height: height)
}
func getReferenceAction(with data: [String: Any]?) -> Action? {
guard let selectedIndex = data?["selectedIndex"] as? Int, let groupArray = data?["referencesGroup"] as? [[String: Any]] else {
return nil
}
let group = groupArray.compactMap { WMFLegacyReference(scriptMessageDict: $0) }
return .reference(selectedIndex: selectedIndex, group: group)
}
func getBackLinkAction(with data: [String: Any]?) -> Action? {
guard
let referenceId = data?["referenceId"] as? String,
let referenceText = data?["referenceText"] as? String,
let backLinkDictionaries = data?["backLinks"] as? [[String: Any]]
else {
return nil
}
let backLinks = backLinkDictionaries.compactMap { ReferenceBackLink(scriptMessageDict: $0) }
return .backLink(referenceId: referenceId, referenceText: referenceText, backLinks: backLinks)
}
func getPronunciationAction(with data: [String: Any]?) -> Action? {
guard let urlString = data?["url"] as? String else {
return nil
}
return .link(href: urlString, text: nil, title: nil)
}
func getFooterItemAction(with data: [String: Any]?) -> Action? {
guard let itemTypeString = data?["itemType"] as? String, let menuItemType = PageContentService.Footer.Menu.Item(rawValue: itemTypeString) else {
return nil
}
return .footerItem(type: menuItemType, payload: data?["payload"])
}
func getScrollToAnchorAction(with data: [String: Any]?) -> Action? {
guard
let dictionary = data?["rect"] as? [String: Any],
let anchor = data?["anchor"] as? String,
let x = dictionary["x"] as? CGFloat,
let y = dictionary["y"] as? CGFloat,
let width = dictionary["width"] as? CGFloat,
let height = dictionary["height"] as? CGFloat,
width > 0,
height > 0
else {
return nil
}
let rect = CGRect(x: x, y: y, width: width, height: height)
return .scrollToAnchor(anchor: anchor, rect: rect)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else {
return
}
guard let actionString = body[bodyActionKey] as? String else {
return
}
let data = body[bodyDataKey] as? [String: Any]
guard let action = PCSAction(pcsActionString: actionString)?.getAction(with: data) else {
// Fallback on href for future unknown event types
if let href = data?["href"] as? String {
let action = ArticleWebMessagingController.Action.unknown(href: href)
delegate?.didRecieve(action: action)
}
return
}
delegate?.didRecieve(action: action)
}
}
// Article as a Living Document Scripts
extension ArticleWebMessagingController {
enum TopBadgeType {
case lastUpdated
case newChanges
}
var articleAsLivingDocBoxSkeletonContainerID: String {
return "significant-changes-skeleton"
}
var badgeHTMLSkeletonContainerID: String {
return "significant-changes-top-skeleton"
}
var articleAsLivingDocBoxContainerID: String {
return "significant-changes-container"
}
var badgeHTMLContainerID: String {
return "significant-changes-top-container"
}
var articleAsLivingDocBoxContainerHTMLStart: String {
return "<div id='\(articleAsLivingDocBoxContainerID)'>"
}
var articleAsLivingDocBoxInnerContainerID: String {
return "significant-changes-inner-container"
}
var articleAsLivingDocBoxInnerContainerHTMLStart: String {
return "<div id='\(articleAsLivingDocBoxInnerContainerID)'>"
}
var articleAsLivingDocBoxInnerContainerHTMLEnd: String {
return "</div>"
}
var articleAsLivingDocBoxContainerHTMLEnd: String {
return "</div>"
}
var badgeHTMLContainerStart: String {
return "<div id='\(badgeHTMLContainerID)'>"
}
var badgeHTMLContainerEnd: String {
return "</div>"
}
func createAndInsertArticleContainerScript(innerHTML: String) -> String {
return """
//then create and insert new element
var pcs = document.getElementById('pcs');
if (!pcs) {
return false;
}
var sections = pcs.getElementsByTagName('section');
if (sections.length === 0) {
return false;
}
var firstSection = sections[0];
var pTags = firstSection.getElementsByTagName('p');
if (pTags.length === 0) {
return false;
}
var firstParagraph = pTags[0];
firstParagraph.insertAdjacentHTML("afterend","\(articleAsLivingDocBoxContainerHTMLStart + innerHTML + articleAsLivingDocBoxContainerHTMLEnd)");
return true;
"""
}
func createAndInsertBadgeScript(innerHTML: String) -> String {
return """
//then create and insert new element
var pcs = document.getElementById('pcs');
if (!pcs) {
return false;
}
var headers = pcs.getElementsByTagName('header');
if (headers.length === 0) {
return false;
}
var firstHeader = headers[0];
firstHeader.insertAdjacentHTML("afterbegin","\(badgeHTMLContainerStart + innerHTML + badgeHTMLContainerEnd)");
return true;
"""
}
func removeArticleAsLivingDocContent(completion: ((Bool) -> Void)? = nil) {
let javascript = """
function removeSignificantEventsContent() {
var boxSkeleton = document.getElementById('\(articleAsLivingDocBoxContainerID)');
var missingSkeletons = false;
if (boxSkeleton) {
boxSkeleton.remove();
} else {
missingSkeletons = true;
}
var topSkeleton = document.getElementById('\(badgeHTMLContainerID)');
if (topSkeleton) {
topSkeleton.remove();
} else {
missingSkeletons = true;
}
if (missingSkeletons == true) {
return false;
} else {
return true;
}
}
removeSignificantEventsContent();
"""
webView?.evaluateJavaScript(javascript) { (result, error) in
DispatchQueue.main.async {
if let error = error {
DDLogDebug("Failure in removeSkeletonArticleAsLivingDocContent: \(error)")
completion?(false)
return
}
if let boolResult = result as? Bool {
if !boolResult {
DDLogDebug("Failure in removeSkeletonArticleAsLivingDocContent")
}
completion?(boolResult)
return
}
DDLogDebug("Failure in removeSkeletonArticleAsLivingDocContent")
completion?(false)
}
}
}
func injectSkeletonArticleAsLivingDocContent(completion: ((Bool) -> Void)? = nil) {
let innerBoxHTML = "<div id='\(articleAsLivingDocBoxSkeletonContainerID)'></div>"
let innerBadgeHTML = "<div id='\(badgeHTMLSkeletonContainerID)'></div>"
let javascript = """
function injectSignificantEventsContent() {
\(createAndInsertArticleContainerScript(innerHTML: innerBoxHTML))
}
injectSignificantEventsContent();
function injectNewChangesBadge() {
\(createAndInsertBadgeScript(innerHTML: innerBadgeHTML))
}
injectNewChangesBadge();
"""
webView?.evaluateJavaScript(javascript) { (result, error) in
DispatchQueue.main.async {
if let error = error {
DDLogDebug("Failure in injectSkeletonArticleAsLivingDocContent: \(error)")
completion?(false)
return
}
if let boolResult = result as? Bool {
if !boolResult {
DDLogDebug("Failure in injectSkeletonArticleAsLivingDocContent")
}
completion?(boolResult)
return
}
DDLogDebug("Failure in injectSkeletonArticleAsLivingDocContent")
completion?(false)
}
}
}
func aaaldContentInsertJS(articleInsertHtmlSnippets: [String]) -> String {
let insertHeaderText = WMFLocalizedString("aaald-article-insert-header", value: "Significant Updates", comment: "Header text in article content insert section that displays recent significant article updates.")
var articleAsLivingDocBoxInnerHTML = "\(articleAsLivingDocBoxInnerContainerHTMLStart)<h4 id='significant-changes-header'>\(insertHeaderText.uppercased(with: Locale.current))</h4><ul id='significant-changes-list'>"
for articleInsertHtmlSnippet in articleInsertHtmlSnippets {
articleAsLivingDocBoxInnerHTML += articleInsertHtmlSnippet
}
let readMoreUpdatesText = WMFLocalizedString("aaald-article-insert-read-more", value: "Read more updates", comment: "Footer text in article content insert section of recent significant article updates that invites user to read more updates on another screen.")
articleAsLivingDocBoxInnerHTML += "</ul><hr id='significant-changes-hr'><p id='significant-changes-read-more'><a href='#significant-events-read-more'>\(readMoreUpdatesText)</a></p>\(articleAsLivingDocBoxInnerContainerHTMLEnd)"
let js = """
function injectSignificantEventsContent() {
//first remove existing element if it's there
var existing = document.getElementById('\(articleAsLivingDocBoxContainerID)');
if (existing) {
existing.innerHTML = "\(articleAsLivingDocBoxInnerHTML)";
return true;
}
\(createAndInsertArticleContainerScript(innerHTML: articleAsLivingDocBoxInnerHTML))
}
injectSignificantEventsContent();
"""
return js
}
func aaaldScrollViewDetectionJS() -> String {
return """
function isInViewport(el) {
const rect = el.getBoundingClientRect();
return (
(rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)) ||
(rect.bottom >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight))
);
}
var aaaldInsertElementForScrollDetection = document.getElementById('\(articleAsLivingDocBoxInnerContainerID)');
var detectedIsOnScreen = false;
function detectIfInsertIsOnScreen() {
if (!aaaldInsertElementForScrollDetection) {
aaaldInsertElementForScrollDetection = document.getElementById('\(articleAsLivingDocBoxInnerContainerID)');
}
if (aaaldInsertElementForScrollDetection) {
const isOnScreen = isInViewport(aaaldInsertElementForScrollDetection);
if (detectedIsOnScreen === false && isOnScreen === true) {
window.webkit.messageHandlers.\(PageContentService.messageHandlerName).postMessage({"\(bodyActionKey)": "\(PCSAction.aaaldInsertOnScreen.rawValue)"});
detectedIsOnScreen = true;
}
}
}
document.addEventListener('scroll', function () {
detectIfInsertIsOnScreen();
}, {
passive: true
});
detectIfInsertIsOnScreen();
"""
}
func aaaldTopBadgeJS(timestamp: String?, topBadgeType: TopBadgeType) -> String {
let newChangesText = WMFLocalizedString("aaald-article-insert-new-changes", value: "New changes", comment: "Badge text in article content showing that there have been new significant updates to the article since the user last viewed it.")
let lastUpdatedText = WMFLocalizedString("aaald-article-insert-last-updated", value: "Last updated", comment: "Badge text in article content showing when the article as last updated.")
guard let timestamp = timestamp else {
return ""
}
let innerContainerID = topBadgeType == .lastUpdated ? "significant-changes-top-inner-container-last-updated" : "significant-changes-top-inner-container-new-changes"
let topTextID = topBadgeType == .lastUpdated ? "significant-changes-top-text-last-updated" : "significant-changes-top-text-new-changes"
let badgeText = topBadgeType == .lastUpdated ? lastUpdatedText : newChangesText
var badgeInnerHTML = "<div id='significant-changes-top-container'><div id='\(innerContainerID)'>"
if topBadgeType == .newChanges {
badgeInnerHTML += "<span id='significant-changes-top-dot'></span>"
}
badgeInnerHTML += "<span id='\(topTextID)'>\(badgeText)</span></div>"
badgeInnerHTML += "<span id='significant-changes-top-timestamp'>\(timestamp)</span>"
return """
function injectNewChangesBadge() {
//first remove existing element if it's there
var existing = document.getElementById('\(badgeHTMLContainerID)');
if (existing) {
existing.innerHTML = "\(badgeInnerHTML)";
return true;
}
\(createAndInsertBadgeScript(innerHTML: badgeInnerHTML))
}
injectNewChangesBadge();
"""
}
func injectArticleAsLivingDocContent(articleInsertHtmlSnippets: [String], topBadgeType: TopBadgeType = .lastUpdated, timestamp: String? = nil, _ completion: ((Bool) -> Void)? = nil) {
guard articleInsertHtmlSnippets.count > 0 else {
completion?(false)
return
}
let contentInsertJS = aaaldContentInsertJS(articleInsertHtmlSnippets: articleInsertHtmlSnippets)
let scrollViewDetectionJS = aaaldScrollViewDetectionJS()
let topBadgeJS = aaaldTopBadgeJS(timestamp: timestamp, topBadgeType: topBadgeType)
let finalInjectJS = contentInsertJS + scrollViewDetectionJS + topBadgeJS
webView?.evaluateJavaScript(finalInjectJS) { (result, error) in
DispatchQueue.main.async {
if let error = error {
DDLogDebug("Failure in injectArticleAsLivingDocContent: \(error)")
completion?(false)
return
}
if let boolResult = result as? Bool {
if !boolResult {
DDLogDebug("Failure in injectArticleAsLivingDocContent")
}
completion?(boolResult)
return
}
DDLogDebug("Failure in injectArticleAsLivingDocContent")
completion?(false)
}
}
}
// should be used only when significant events is active
// we are manually suppressing left and right body margins in standard view
// and adding back in as padding so we get the edge to edge gray background
func customUpdateMargins(with layoutMargins: UIEdgeInsets, leadImageHeight: CGFloat) {
let javascript = """
function customUpdateMargins() {
document.body.style.marginLeft = "0px";
document.body.style.marginRight = "0px";
document.body.style.paddingLeft = "\(layoutMargins.left)px";
document.body.style.paddingRight = "\(layoutMargins.right)px";
document.body.style.marginTop = "\(leadImageHeight + layoutMargins.top)px";
var seContainer = document.getElementById('\(articleAsLivingDocBoxContainerID)');
if (seContainer) {
seContainer.style.marginLeft = "-\(layoutMargins.left)px";
seContainer.style.marginRight = "-\(layoutMargins.right)px";
seContainer.style.paddingLeft = "\(layoutMargins.left)px";
seContainer.style.paddingRight = "\(layoutMargins.right)px";
}
return true;
}
customUpdateMargins();
"""
webView?.evaluateJavaScript(javascript) { (success, error) in
if let error = error {
DDLogDebug("Failure in customUpdateMargins: \(error)")
}
if let success = success as? Bool,
success == false {
DDLogDebug("Failure in customUpdateMargins")
}
}
}
}